Browse Source

new: Resolver - non static methods (get,set,expression)
new: Resolver - static method "compile" returns precompiled resolver (better performance, no regex matching every time)
new: Resolver - serialization (proxy pattern)
new: Resolver - support for array-like objects: CharSequence, java.util.regex.Matcher

new: ResolveFormatter - new class

regression: disabled EventLockTest (unstable)

Ranides Atterwim 11 năm trước cách đây
mục cha
commit
9cbb74dedc

+ 1 - 1
pom.xml

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

+ 191 - 86
src/main/java/net/ranides/assira/reflection/Resolver.java

@@ -6,120 +6,113 @@
  */
 package net.ranides.assira.reflection;
 
+import java.io.Serializable;
+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.text.Strings;
 
 /**
- * Klasa do wyciągania z obiektów wartości na podstawie wyrażenia
- * pseudo-javowego. 
- * @author Ranides Atterwim <ranides@gmail.com>
+ * Compiled expression usable for read/write value from objects. 
+ * You can access recursively properties from properties. 
+ * 
+ * <h3>Supported syntax:</h3>
+ * <table>
+ *  <tr><td>.name</td><td>read field or bean-property from object</td></tr>
+ *  <tr><td>name</td><td>read field or bean-property from object</td></tr>
+ *  <tr><td>#name</td><td>get key from a {@link Map}</td></tr>
+ *  <tr><td>[number]</td><td>get item from an array-like object</td></tr>
+ *  <tr><td>number</td><td>get item from an array-like object</td></tr>
+ * </table>
+ * 
+ * <h3>Supported array-like objects:</h3>
+ * <ul>
+ *  <li>{@link List}</li>
+ *  <li>array</li>
+ *  <li>{@link Matcher}</li>
+ * </li>
+ * 
+ * <h3>Example:</h3>
+ * <pre>
+ *      object.property.list[4].users#id.name
+ * </pre>
+ *
  */
-public final class Resolver {
+public final class Resolver implements Serializable {
+    
+    private static final long serialVersionUID = 1L;
     
     private static final Pattern RE_RESOLVE = Pattern.compile(
         "(?:\\G\\.?([^\\.\\[\\]#]+))|(?:\\G#([^\\.\\[\\]#]+))|(?:\\G\\[([0-9]+)\\])"
     );
     
-    private Resolver() {
-        // utility class
-    }
-    
-    /**
-     * Returns value of variable resolved by expression. 
-     * You can access recursively properties from properties. Supported syntax:
-     * <table>
-     *  <tr><td>.name</td><td>read field or bean-property from object</td></tr>
-     *  <tr><td>name</td><td>read field or bean-property from object</td></tr>
-     *  <tr><td>#name</td><td>get key from a {@link Map}</td></tr>
-     *  <tr><td>[number]</td><td>get item from a {@link List} or array</td></tr>
-     *  <tr><td>number</td><td>get item from a {@link List} or array</td></tr>
-     * </table>
-     * <pre>
-     *      object.property.list[4].users#id.name
-     * </pre>
-     * @param object
-     * @param path
-     * @return 
-     */
-    public static Object get(Object object, String path) {
-        return resolve(object, path, false, null);
-    }
-    
-    /**
-     * Changes value of variable resolved by expression. 
-     * You can access recursively properties from properties. Supported syntax:
-     * <table>
-     *  <tr><td>.name</td><td>read field or bean-property from object</td></tr>
-     *  <tr><td>name</td><td>read field or bean-property from object</td></tr>
-     *  <tr><td>#name</td><td>get key from a {@link Map}</td></tr>
-     *  <tr><td>[number]</td><td>get item from a {@link List} or array</td></tr>
-     *  <tr><td>number</td><td>get item from a {@link List} or array</td></tr>
-     * </table>
-     * <pre>
-     *      object.property.list[4].users#id.name
-     * </pre>
-     * 
-     * @param object
-     * @param path
-     * @param value
-     * @return 
-     */
-    public static Object set(Object object, String path, Object value) {
-        return resolve(object, path, true, value);
-    }
-    
-    private static Object resolve(Object object, String path, boolean setmode, Object value) {
-        Object context = object;
-        Matcher hit = RE_RESOLVE.matcher(path);
+    private final String expression;
+    private final Token[] tokens;
+    
+    private Resolver(String expression) {
+        this.expression = expression;
+
+        ArrayList<Token> list = new ArrayList<>();
+        Matcher hit = RE_RESOLVE.matcher(expression);
         while(hit.find()) {
-            boolean last = (path.length() == hit.end());
-            
-            if(last && setmode) {
-                Resolver.set(context, hit, value);
-                return value;
-            }
-            context = Resolver.get(context, hit);
-            if(last && !setmode) {
-                return context;
+            list.add(compileToken(hit));
+            if(expression.length() == hit.end()) {
+                tokens = list.toArray(new Token[list.size()]);
+                return;
             }
         }
         throw new IllegalArgumentException("Invalid expression");
     }
-
-    private static Object get(Object context, Matcher hit) throws NumberFormatException {
-        if( isint(hit.group(1)) ) {
-            return ArrayUtils.wrap(context).get( Integer.parseInt(hit.group(1)) );
-        }
-        if( null != hit.group(1) ) {
-            return BeanModel.fromObject(context).get(context, hit.group(1));
-        }
-        if( null != hit.group(2) ) {
-            return ((Map<?,?>)context).get(hit.group(2));
+    
+    public static Resolver compile(String expression) {
+        return new Resolver(expression);
+    }
+    
+    public static Object get(Object object, String expression) {
+        return Resolver.compile(expression).get(object);
+    }
+    
+    public static Object set(Object object, String expression, Object value) {
+        return Resolver.compile(expression).set(object, value);
+    }
+    
+    public Object get(Object object) {
+        Object context = object;
+        for(Token token : tokens) {
+            context = token.get(context);
         }
-        if( null != hit.group(3) ) {
-            return ArrayUtils.wrap(context).get( Integer.parseInt(hit.group(3)) );
+        return context;
+    }
+    
+    public Object 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);
         }
-        throw new IllegalArgumentException("Invalid expression fragment:" + hit.group());
+        tokens[n].set(context, value);
+        return context;
+    }
+    
+    public String expression() {
+        return expression;
     }
     
-    private static void set(Object context, Matcher hit, Object value) throws NumberFormatException {
+    private static Token compileToken(Matcher hit) throws NumberFormatException {
         if( isint(hit.group(1)) ) {
-            ArrayUtils.wrap(context).set(Integer.parseInt(hit.group(1)), value);
-            return;
+            return new AToken(hit.group(1));
         }
         if( null != hit.group(1) ) {
-            BeanModel.fromObject(context).put(context, hit.group(1), value);
-            return;
+            return new BToken(hit.group(1));
         }
         if( null != hit.group(2) ) {
-            ((Map<String,Object>)context).put(hit.group(2),value);
-            return;
+            return new MToken(hit.group(2));
         }
         if( null != hit.group(3) ) {
-            ArrayUtils.wrap(context).set(Integer.parseInt(hit.group(3)), value);
-            return;
+            return new AToken(hit.group(3));
         }
         throw new IllegalArgumentException("Invalid expression fragment:" + hit.group());
     }
@@ -136,4 +129,116 @@ public final class Resolver {
         return true;
     }
     
+    @Override
+    public int hashCode() {
+        return expression.hashCode();
+    }
+
+    @Override
+    public boolean equals(Object object) {
+        if(!(object instanceof Resolver)) {
+            return false;
+        }
+        Resolver other = (Resolver)object;
+        return Strings.isEqual(this.expression, other.expression);
+    }
+    
+    private Object writeReplace() {
+        return new SerializationProxy(this);
+    }
+    
+    private static class SerializationProxy implements Serializable {
+        
+        String expression;
+
+        public SerializationProxy(Resolver source) {
+            this.expression = source.expression;
+        }
+        
+        private Object readResolve() {
+            return new Resolver(expression);
+        }
+        
+    }
+    
+    
+    private interface Token {
+        
+        Object get(Object context);
+        
+        void set(Object context, Object value);
+        
+    }
+    
+    private static class AToken implements Token {
+        
+        private final int index;
+
+        public AToken(String index) {
+            this.index = Integer.parseInt(index);
+        }
+
+        @Override
+        public Object get(Object context) {
+            if(context instanceof Matcher) {
+                return ((Matcher)context).group(index);
+            }
+            if(context instanceof CharSequence) {
+                return ((CharSequence)context).charAt(index);
+            }
+            return ArrayUtils.wrap(context).get(index);
+        }
+
+        @Override
+        public void set(Object context, Object value) {
+            if(context instanceof Matcher) {
+                throw new UnsupportedOperationException("Can't modify object: Matcher");
+            }
+            if(context instanceof CharSequence) {
+                throw new UnsupportedOperationException("Can't modify object: CharSequence");
+            }
+            ArrayUtils.wrap(context).set(index, value);
+        }
+        
+    }
+    
+    private static class BToken implements Token {
+        
+        private final String index;
+
+        public BToken(String index) {
+            this.index = index;
+        }
+
+        @Override
+        public Object get(Object context) {
+            return BeanModel.fromObject(context).get(context, index);
+        }
+
+        @Override
+        public void set(Object context, Object value) {
+            BeanModel.fromObject(context).put(context, index, value);
+        }
+                
+    }
+    
+    private static class MToken implements Token {
+
+        private final String key;
+
+        public MToken(String key) {
+            this.key = key;
+        }
+
+        @Override
+        public Object get(Object context) {
+            return ((Map<?,?>)context).get(key);
+        }
+
+        @Override
+        public void set(Object context, Object value) {
+            ((Map<String,Object>)context).put(key, value);
+        }
+        
+    }
 }

+ 0 - 204
src/main/java/net/ranides/assira/text/NamedFormatter.java

@@ -1,204 +0,0 @@
-/*
- *  @author Ranides Atterwim <ranides@gmail.com>
- *  @copyright Ranides Atterwim
- *  @license WTFPL
- *  @url http://ranides.net/projects/assira
- */
-package net.ranides.assira.text;
-
-import java.io.*;
-import java.util.Formatter;
-import java.util.HashMap;
-import java.util.Locale;
-import java.util.Map;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
-import java.util.regex.PatternSyntaxException;
-import net.ranides.assira.collection.ArrayUtils;
-import net.ranides.assira.generic.Function;
-import net.ranides.assira.reflection.ObjectInspector;
-
-/**
- *
- * @author ranides
- */
-public class NamedFormatter implements Closeable, Flushable {
-
-    public static class Args extends HashMap<String, Object> {
-
-        @SuppressWarnings("PMD.MethodNameRules")
-        public void $(String key, Object value) {
-            put(key, value);
-        }
-    }
-
-    //<editor-fold defaultstate="collapsed" desc="comment">
-    
-    public NamedFormatter() {
-        result = new Formatter();
-    }
-    
-    public NamedFormatter(Appendable target) {
-        result = new Formatter(target);
-    }
-    
-    public NamedFormatter(Appendable target, Locale locale) {
-        result = new Formatter(target, locale);
-    }
-    
-    public NamedFormatter(File file) throws FileNotFoundException {
-        try {
-            result = new Formatter(file, TextEncoding.IO_UTF8);
-        } catch(UnsupportedEncodingException nex) {
-            throw new AssertionError(nex);
-        }
-    }
-    
-    public NamedFormatter(File file, String charset) throws FileNotFoundException, UnsupportedEncodingException {
-        result = new Formatter(file, charset);
-    }
-    
-    public NamedFormatter(File file, String charset, Locale locale) throws FileNotFoundException, UnsupportedEncodingException {
-        result = new Formatter(file, charset, locale);
-    }
-    
-    public NamedFormatter(Locale locale) {
-        result = new Formatter(locale);
-    }
-    
-    public NamedFormatter(OutputStream stream) {
-        try {
-            result = new Formatter(stream, TextEncoding.IO_UTF8);
-        } catch(UnsupportedEncodingException nex) {
-            throw new AssertionError(nex);
-        }
-    }
-    
-    public NamedFormatter(OutputStream stream, String charset) throws UnsupportedEncodingException {
-        result = new Formatter(stream, charset);
-    }
-    
-    public NamedFormatter(OutputStream stream, String charset, Locale locale) throws UnsupportedEncodingException {
-        result = new Formatter(stream, charset, locale);
-    }
-    
-    public NamedFormatter(PrintStream stream) {
-        result = new Formatter(stream);
-    }
-    
-    public NamedFormatter(String fileName) throws FileNotFoundException {
-        try {
-            result = new Formatter(fileName, TextEncoding.IO_UTF8);
-        } catch(UnsupportedEncodingException nex) {
-            throw new AssertionError(nex);
-        }
-    }
-    
-    public NamedFormatter(String fileName, String csn) throws FileNotFoundException, UnsupportedEncodingException {
-        result = new Formatter(fileName, csn);
-    }
-    
-    public NamedFormatter(String fileName, String charset, Locale locale) throws FileNotFoundException, UnsupportedEncodingException {
-        result = new Formatter(fileName, charset, locale);
-    }
-//</editor-fold>
-
-    @Override
-    public void close() {
-        result.close();
-    }
-
-    @Override
-    public void flush() {
-        result.flush();
-    }
-
-    public NamedFormatter format(Locale locale, String format, Object args, Object... params) {
-        String str = format;
-        if (null != args) {
-            if (args instanceof Map) {
-                str = preformat(format, (Map) args);
-            } else {
-                str = preformat(format, args);
-            }
-        }
-        if (ArrayUtils.isEmpty(params)) {
-            result.format(locale, "%s", str);
-        } else {
-            result.format(locale, str, params);
-        }
-        return this;
-    }
-
-    public NamedFormatter format(String format, Object args, Object... params) {
-        return this.format(Locale.getDefault(), format, args, params);
-    }
-
-    public static String str(Locale lang, String format, Object args, Object... params) {
-        return new NamedFormatter().format(lang, format, args, params).toString();
-    }
-
-    public static String str(String format, Object args, Object... params) {
-        return new NamedFormatter().format(format, args, params).toString();
-    }
-
-    public static Pattern compile(Locale lang, String format, Object args, Object... params) {
-        try {
-            return Pattern.compile(new NamedFormatter().format(lang, format, args, params).toString());
-        } catch (PatternSyntaxException cause) {
-            throw new AssertionError(cause);
-        }
-    }
-
-    public static Pattern compile(String format, Object args, Object... params) {
-        try {
-            return Pattern.compile(new NamedFormatter().format(format, args, params).toString());
-        } catch (PatternSyntaxException cause) {
-            throw new AssertionError(cause);
-        }
-    }
-
-    public IOException ioException() {
-        return result.ioException();
-    }
-
-    public Locale locale() {
-        return result.locale();
-    }
-
-    public Appendable out() {
-        return result.out();
-    }
-
-    @Override
-    public String toString() {
-        return result.toString();
-    }
-
-    private final Formatter result;
-
-    private static final Pattern RE_NAME = Pattern.compile("(\\\\/\\{([^\\\\\\{\\}]*?)\\})|(\\\\\\{)|\\{([^\\\\\\{\\}]*?)\\}");
-
-    private String preformat(String format, final Object args) {
-
-        return StringUtils.replace(format, RE_NAME, new Function<String,Matcher>() {
-            @Override
-            public String apply(Matcher matcher) {
-                if (null != matcher.group(3)) {
-                    return null;
-                }
-                String key = null == matcher.group(1) ? matcher.group(4) : matcher.group(2);
-                return String.valueOf( ObjectInspector.getField(args, key) );
-            }
-        });
-    }
-
-    private String preformat(String format, final Map<?, ?> args) {
-        return StringUtils.replace(format, RE_NAME, new Function<String,Matcher>() {
-            @Override
-            public String apply(Matcher matcher) {
-                return args.get(matcher.group(4)).toString();
-            }
-        });
-    }
-}

+ 153 - 0
src/main/java/net/ranides/assira/text/ResolveFormatter.java

@@ -0,0 +1,153 @@
+/*
+ * @author Ranides Atterwim <ranides@gmail.com>
+ * @copyright Ranides Atterwim
+ * @license WTFPL
+ * @url http://ranides.net/projects/assira
+ */
+package net.ranides.assira.text;
+
+import java.io.IOException;
+import java.io.InvalidObjectException;
+import java.io.ObjectInputStream;
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import net.ranides.assira.generic.ValueUtils;
+import net.ranides.assira.math.HashHelper;
+import net.ranides.assira.reflection.Resolver;
+
+/**
+ *
+ * @author Ranides Atterwim <ranides@gmail.com>
+ */
+public class ResolveFormatter implements Serializable {
+    
+    private static final long serialVersionUID = 0L;
+    
+    private static final Pattern RE = Pattern.compile("\\{(.+?)\\}");
+    
+    private final String pattern;
+    private final Token[] tokens;
+
+    private ResolveFormatter(String pattern) {
+        this.pattern = pattern;
+        
+        List<Token> list = new ArrayList<>();
+        Matcher hit = RE.matcher(pattern);
+        int last = 0;
+        while(hit.find()) {
+            if(hit.start()-last > 0) {
+                list.add(new SToken(pattern.substring(last,hit.start())));
+            }
+            list.add(new RToken(hit.group(1)));
+            last = hit.end();
+        }
+        if(last<pattern.length()) {
+            list.add(new SToken(pattern.substring(last)));
+        }
+        this.tokens = list.toArray(new Token[list.size()]);
+    }
+    
+    public static ResolveFormatter compile(String pattern) {
+        return new ResolveFormatter(pattern);
+    }
+    
+    public static Appendable formatTo(Appendable target, String pattern, Object context) throws IOException {
+        return compile(pattern).formatTo(target, context);
+    }
+    
+    public static String format(String pattern, Object context) {
+        return compile(pattern).format(context);
+    }
+    
+    public Appendable formatTo(Appendable target, Object context) throws IOException {
+        for(Token token : tokens) {
+            token.append(target, context);
+        }
+        return target;
+    }
+    
+    public String format(Object context) {
+        try {
+            return formatTo(new StringBuilder(), context).toString();
+        } catch (IOException ex) {
+            // that should never happen
+            throw new AssertionError(ex);
+        }
+    }
+    
+    public String pattern() {
+        return pattern;
+    }
+
+    @Override
+    public int hashCode() {
+        return HashHelper.hashValues(7, 19, pattern);
+    }
+
+    @Override
+    public boolean equals(Object object) {
+        if(!(object instanceof ResolveFormatter)) {
+            return false;
+        }
+        ResolveFormatter other = (ResolveFormatter)object;
+        return ValueUtils.isEqual(this.pattern, other.pattern);
+    }
+    
+    private Object writeReplace() {
+        return new SerializationProxy(this);
+    }
+    
+    private static class SerializationProxy implements Serializable {
+        
+        String pattern;
+
+        public SerializationProxy(ResolveFormatter source) {
+            this.pattern = source.pattern;
+        }
+        
+        private Object readResolve() {
+            return new ResolveFormatter(pattern);
+        }
+        
+    }
+            
+    private interface Token {
+        
+        void append(Appendable target, Object context) throws IOException;
+        
+    }
+    
+    private static final class SToken implements Token {
+        
+        private final String value;
+
+        public SToken(String value) {
+            this.value = value;
+        }
+
+        @Override
+        public void append(Appendable target, Object context) throws IOException {
+            target.append(value);
+        }
+        
+    }
+    
+    private static final class RToken implements Token {
+        
+        private final Resolver resolver;
+
+        public RToken(String expression) {
+            this.resolver = Resolver.compile(expression);
+        }
+
+        @Override
+        public void append(Appendable target, Object context) throws IOException {
+            target.append(String.valueOf( resolver.get(context)));
+        }
+        
+    }
+    
+}

+ 1 - 0
src/test/java/net/ranides/assira/events/EventLockTest.java

@@ -28,6 +28,7 @@ public class EventLockTest {
      * opóźnienie 50ms może być za małe, żeby uniknąć race condition
      * 
      */
+    @Ignore("unstable") // @todo (assira # 4) stabilize that unit test
     @Test
     public void testEventLockRace() throws InterruptedException {
         final EventReactor main = EventReactor.newInstance("name", 32, 1000);

+ 30 - 0
src/test/java/net/ranides/assira/reflection/ResolverTest.java

@@ -6,10 +6,16 @@
  */
 package net.ranides.assira.reflection;
 
+import java.io.IOException;
 import java.util.Arrays;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
+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.*;
 
@@ -19,6 +25,15 @@ import static org.junit.Assert.*;
  */
 public class ResolverTest {
     
+    @Test
+    public void testSerialization() throws IOException {
+        Resolver a = Resolver.compile("content.item.height");
+        Resolver b = Serializer.copy(a);
+        
+        assertNotSame(a, b);
+        assertEquals(a, b);
+    }
+    
     @Test
     public void testResolve() {
         A context = new A();
@@ -129,6 +144,21 @@ public class ResolverTest {
         
     }
     
+    @Test
+    public void testMatcher() {
+        Matcher hit = Pattern.compile("([a-z]+) ([a-z]+) ([a-z]+)").matcher("one two last");
+        assertTrue(hit.find());
+        assertEquals("two", Resolver.get(hit, "2"));
+    }
+    
+    @Test
+    public void testCharSequence() {
+        String a = "aeRto";
+        AnsiString b = new AnsiString("uAdd");
+        assertEquals('R', Resolver.get(a, "2"));
+        assertEquals('A', Resolver.get(b, "1"));
+    }
+    
     private static class A {
         public int x = 5;
         public int y = 8;

+ 73 - 0
src/test/java/net/ranides/assira/text/ResolveFormatterTest.java

@@ -0,0 +1,73 @@
+/*
+ * @author Ranides Atterwim <ranides@gmail.com>
+ * @copyright Ranides Atterwim
+ * @license WTFPL
+ * @url http://ranides.net/projects/assira
+ */
+package net.ranides.assira.text;
+
+import java.io.IOException;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import net.ranides.assira.generic.Serializer;
+import org.junit.Test;
+import static org.junit.Assert.*;
+
+/**
+ *
+ * @author Ranides Atterwim <ranides@gmail.com>
+ */
+public class ResolveFormatterTest {
+    
+    public ResolveFormatterTest() {
+    }
+    
+    @Test
+    public void testSerialization() throws IOException {
+        ResolveFormatter a = ResolveFormatter.compile("Hello {1} world!");
+        ResolveFormatter b = Serializer.copy(a);
+        
+        assertNotSame(a, b);
+        assertEquals(a, b);
+    }
+
+    @Test
+    public void testArray() {
+        String ret = ResolveFormatter.format("Hello {1} world {2}{3}!", new int[]{17, 19, 47, 33});
+        assertEquals("Hello 19 world 4733!", ret);
+    }
+    
+    @Test
+    public void testMatcher() {
+        String source ="AH-ello";
+        Matcher hit = Pattern.compile("(.)(.)-(.+)").matcher(source);
+        assertTrue(hit.find());
+        
+        String ret = ResolveFormatter.format("Hello {1} world {2}{3}!", hit);
+        assertEquals("Hello A world Hello!", ret);
+    }
+    
+    @Test
+    public void testComplex() {
+        String source ="XY-point";
+        Matcher hit = Pattern.compile("(.)(.)-(.+)").matcher(source);
+        assertTrue(hit.find());
+        
+        Wrapper wrapper = new Wrapper("Hello", 77, hit, new Wrapper("world", '?'));
+        String r1 = ResolveFormatter.format("{[0]} {[1]} {[2][3]} {[3].items[0]} {[3].items[1]}", wrapper.items);
+        assertEquals("Hello 77 point world ?", r1);
+        
+        String r2 = ResolveFormatter.format("{0} {1} {2.3.4} {3.items.0} {3.items.1}", wrapper.items);
+        assertEquals("Hello 77 t world ?", r2);
+    }
+    
+    private static final class Wrapper {
+        public Object[] items;
+
+        public Wrapper(Object... items) {
+            this.items = items;
+        }
+        
+    }
+    
+}