use stringified implementation for MethodSignature, make economically serializable

git-svn-id: https://svn.apache.org/repos/asf/commons/proper/proxy/branches/version-2.0-work@1510072 13f79535-47bb-0310-9956-ffa450edef68
diff --git a/core/src/main/java/org/apache/commons/proxy2/impl/MethodSignature.java b/core/src/main/java/org/apache/commons/proxy2/impl/MethodSignature.java
index bfbf268..05cad71 100644
--- a/core/src/main/java/org/apache/commons/proxy2/impl/MethodSignature.java
+++ b/core/src/main/java/org/apache/commons/proxy2/impl/MethodSignature.java
@@ -17,12 +17,22 @@
 

 package org.apache.commons.proxy2.impl;

 

-import org.apache.commons.lang3.builder.EqualsBuilder;

-import org.apache.commons.lang3.builder.HashCodeBuilder;

-

+import java.io.Serializable;

+import java.lang.reflect.Array;

 import java.lang.reflect.Method;

-import java.util.Arrays;

+import java.text.ParsePosition;

+import java.util.ArrayList;

+import java.util.Collections;

+import java.util.HashMap;

 import java.util.List;

+import java.util.Map;

+

+import org.apache.commons.lang3.ArrayUtils;

+import org.apache.commons.lang3.StringUtils;

+import org.apache.commons.lang3.Validate;

+import org.apache.commons.lang3.builder.HashCodeBuilder;

+import org.apache.commons.lang3.reflect.MethodUtils;

+import org.apache.commons.lang3.tuple.Pair;

 

 /**

  * A class for capturing the signature of a method (its name and parameter types).

@@ -30,14 +40,149 @@
  * @author James Carman

  * @since 1.0

  */

-public class MethodSignature

+public class MethodSignature implements Serializable

 {

+    private static final long serialVersionUID = 1L;

+

+    private static final Map<Class<?>, Character> PRIMITIVE_ABBREVIATIONS;

+    private static final Map<Character, Class<?>> REVERSE_ABBREVIATIONS;

+    static

+    {

+        final Map<Class<?>, Character> primitiveAbbreviations = new HashMap<Class<?>, Character>();

+        primitiveAbbreviations.put(Boolean.TYPE, Character.valueOf('Z'));

+        primitiveAbbreviations.put(Byte.TYPE, Character.valueOf('B'));

+        primitiveAbbreviations.put(Short.TYPE, Character.valueOf('S'));

+        primitiveAbbreviations.put(Integer.TYPE, Character.valueOf('I'));

+        primitiveAbbreviations.put(Character.TYPE, Character.valueOf('C'));

+        primitiveAbbreviations.put(Long.TYPE, Character.valueOf('J'));

+        primitiveAbbreviations.put(Float.TYPE, Character.valueOf('F'));

+        primitiveAbbreviations.put(Double.TYPE, Character.valueOf('D'));

+        primitiveAbbreviations.put(Void.TYPE, Character.valueOf('V'));

+        final Map<Character, Class<?>> reverseAbbreviations = new HashMap<Character, Class<?>>();

+        for (Map.Entry<Class<?>, Character> e : primitiveAbbreviations.entrySet())

+        {

+            reverseAbbreviations.put(e.getValue(), e.getKey());

+        }

+        PRIMITIVE_ABBREVIATIONS = Collections.unmodifiableMap(primitiveAbbreviations);

+        REVERSE_ABBREVIATIONS = Collections.unmodifiableMap(reverseAbbreviations);

+    }

+

+    private static void appendTo(StringBuilder buf, Class<?> type)

+    {

+        if (type.isPrimitive())

+        {

+            buf.append(PRIMITIVE_ABBREVIATIONS.get(type));

+        }

+        else if (type.isArray())

+        {

+            buf.append('[');

+            appendTo(buf, type.getComponentType());

+        }

+        else

+        {

+            buf.append('L').append(type.getName().replace('.', '/')).append(';');

+        }

+    }

+

+    private static class SignaturePosition extends ParsePosition

+    {

+        SignaturePosition() {

+            super(0);

+        }

+

+        SignaturePosition next()

+        {

+            return plus(1);

+        }

+

+        SignaturePosition plus(int addend)

+        {

+            setIndex(getIndex() + addend);

+            return this;

+        }

+    }

+

+    private static Pair<String, Class<?>[]> parse(String internal)

+    {

+        Validate.notBlank(internal, "Cannot parse blank method signature");

+        final SignaturePosition pos = new SignaturePosition();

+        int lparen = internal.indexOf('(', pos.getIndex());

+        Validate.isTrue(lparen > 0, "Method signature \"%s\" requires parentheses", internal);

+        final String name = internal.substring(0, lparen).trim();

+        Validate.notBlank(name, "Method signature \"%s\" has blank name", internal);

+

+        pos.setIndex(lparen + 1);

+

+        boolean complete = false;

+        final List<Class<?>> params = new ArrayList<Class<?>>();

+        while (pos.getIndex() < internal.length())

+        {

+            final char c = internal.charAt(pos.getIndex());

+            if (Character.isWhitespace(c)) {

+                pos.next();

+                continue;

+            }

+            final Character k = Character.valueOf(c);

+            if (REVERSE_ABBREVIATIONS.containsKey(k))

+            {

+                params.add(REVERSE_ABBREVIATIONS.get(k));

+                pos.next();

+                continue;

+            }

+            if (')' == c)

+            {

+                complete = true;

+                pos.next();

+                break;

+            }

+            try {

+                params.add(parseType(internal, pos));

+            } catch (ClassNotFoundException e) {

+                throw new IllegalArgumentException(String.format("Method signature \"%s\" references unknown type",

+                    internal), e);

+            }

+        }

+        Validate.isTrue(complete, "Method signature \"%s\" is incomplete", internal);

+        Validate.isTrue(StringUtils.isBlank(internal.substring(pos.getIndex())),

+            "Method signature \"%s\" includes unrecognized content beyond end", internal);

+

+        return Pair.of(name, params.toArray(ArrayUtils.EMPTY_CLASS_ARRAY));

+    }

+

+    private static Class<?> parseType(String internal, SignaturePosition pos) throws ClassNotFoundException {

+        final int here = pos.getIndex();

+        final char c = internal.charAt(here);

+

+        switch (c)

+        {

+        case '[':

+            pos.next();

+            final Class<?> componentType = parseType(internal, pos);

+            return Array.newInstance(componentType, 0).getClass();

+        case 'L':

+            pos.next();

+            final int type = pos.getIndex();

+            final int semi = internal.indexOf(';', type);

+            Validate.isTrue(semi > 0, "Type at index %s of method signature \"%s\" not terminated by semicolon", here,

+                internal);

+            final String className = internal.substring(type, semi).replace('/', '.');

+            Validate.notBlank(className, "Invalid classname at position %s of method signature \"%s\"", type, internal);

+            pos.setIndex(semi + 1);

+            return Class.forName(className);

+        default:

+            throw new IllegalArgumentException(String.format(

+            "Unexpected character at index %s of method signature \"%s\"", here, internal));

+        }

+    }

+

 //----------------------------------------------------------------------------------------------------------------------

 // Fields

 //----------------------------------------------------------------------------------------------------------------------

 

-    private final String name;

-    private final List<Class<?>> parameterTypes;

+    /**

+     * Stored as a Java method descriptor minus return type.

+     */

+    private final String internal;

 

 //----------------------------------------------------------------------------------------------------------------------

 // Constructors

@@ -50,8 +195,29 @@
      */

     public MethodSignature(Method method)

     {

-        this.name = method.getName();

-        this.parameterTypes = Arrays.asList(method.getParameterTypes());

+        final StringBuilder buf = new StringBuilder(method.getName()).append('(');

+        for (Class<?> p : method.getParameterTypes())

+        {

+            appendTo(buf, p);

+        }

+        buf.append(')');

+        this.internal = buf.toString();

+    }

+

+//----------------------------------------------------------------------------------------------------------------------

+// Methods

+//----------------------------------------------------------------------------------------------------------------------

+

+    /**

+     * Get the corresponding {@link Method} instance

+     * from the specified {@link Class}.

+     * @param type

+     * @return Method

+     */

+    public Method toMethod(Class<?> type)

+    {

+        final Pair<String,Class<?>[]> info = parse(internal);

+        return MethodUtils.getAccessibleMethod(type, info.getLeft(), info.getRight());

     }

 

 //----------------------------------------------------------------------------------------------------------------------

@@ -76,10 +242,7 @@
             return false;

         }

         MethodSignature other = (MethodSignature) o;

-        return new EqualsBuilder()

-                .append(name, other.name)

-                .append(parameterTypes, other.parameterTypes)

-                .build();

+        return other.internal.equals(internal);

     }

 

     /**

@@ -87,6 +250,14 @@
      */

     public int hashCode()

     {

-        return new HashCodeBuilder().append(name).append(parameterTypes).build();

+        return new HashCodeBuilder().append(internal).build();

+    }

+

+    /**

+     * {@inheritDoc}

+     */

+    public String toString()

+    {

+        return internal;

     }

 }

diff --git a/core/src/test/java/org/apache/commons/proxy2/impl/MethodSignatureTest.java b/core/src/test/java/org/apache/commons/proxy2/impl/MethodSignatureTest.java
index c79b34e..a4471ee 100644
--- a/core/src/test/java/org/apache/commons/proxy2/impl/MethodSignatureTest.java
+++ b/core/src/test/java/org/apache/commons/proxy2/impl/MethodSignatureTest.java
@@ -17,9 +17,14 @@
 
 package org.apache.commons.proxy2.impl;
 
+import java.lang.reflect.Method;
+
+import org.apache.commons.lang3.SerializationUtils;
+import org.apache.commons.proxy2.util.AbstractEcho;
 import org.apache.commons.proxy2.util.AbstractTestCase;
 import org.apache.commons.proxy2.util.DuplicateEcho;
 import org.apache.commons.proxy2.util.Echo;
+import org.apache.commons.proxy2.util.EchoImpl;
 import org.junit.Test;
 
 import static org.junit.Assert.*;
@@ -33,12 +38,52 @@
     @Test
     public void testEquals() throws Exception
     {
-        final MethodSignature sig = new MethodSignature(Echo.class.getMethod("echoBack", new Class[] {String.class}));
+        final MethodSignature sig = new MethodSignature(Echo.class.getMethod("echoBack", String.class));
         assertTrue(sig.equals(sig));
         assertFalse(sig.equals("echoBack"));
-        assertEquals(sig, new MethodSignature(Echo.class.getMethod("echoBack", new Class[] {String.class})));
-        assertEquals(sig, new MethodSignature(DuplicateEcho.class.getMethod("echoBack", new Class[] {String.class})));
-        assertFalse(sig.equals(new MethodSignature(Echo.class.getMethod("echoBack", new Class[] {String.class, String.class}))));
-        assertFalse(sig.equals(new MethodSignature(Echo.class.getMethod("echo", new Class[] {}))));
+        assertEquals(sig, new MethodSignature(Echo.class.getMethod("echoBack", String.class)));
+        assertEquals(sig, new MethodSignature(DuplicateEcho.class.getMethod("echoBack", String.class)));
+        assertFalse(sig.equals(new MethodSignature(Echo.class.getMethod("echoBack", String.class, String.class))));
+        assertFalse(sig.equals(new MethodSignature(Echo.class.getMethod("echo"))));
+    }
+
+    @Test
+    public void testSerialization() throws Exception
+    {
+        final MethodSignature sig = new MethodSignature(Echo.class.getMethod("echoBack", String.class));
+        assertEquals(sig, SerializationUtils.clone(sig));
+    }
+
+    @Test
+    public void testToString() throws Exception
+    {
+        assertEquals("echo()", new MethodSignature(Echo.class.getMethod("echo")).toString());
+        assertEquals("echoBack(Ljava/lang/String;)", new MethodSignature(Echo.class.getMethod("echoBack", String.class)).toString());
+        assertEquals("echoBack([Ljava/lang/String;)", new MethodSignature(Echo.class.getMethod("echoBack", String[].class)).toString());
+        assertEquals("echoBack([[Ljava/lang/String;)", new MethodSignature(Echo.class.getMethod("echoBack", String[][].class)).toString());
+        assertEquals("echoBack([[[Ljava/lang/String;)", new MethodSignature(Echo.class.getMethod("echoBack", String[][][].class)).toString());
+        assertEquals("echoBack(I)", new MethodSignature(Echo.class.getMethod("echoBack", int.class)).toString());
+        assertEquals("echoBack(Z)", new MethodSignature(Echo.class.getMethod("echoBack", boolean.class)).toString());
+        assertEquals("echoBack(Ljava/lang/String;Ljava/lang/String;)", new MethodSignature(Echo.class.getMethod("echoBack", String.class, String.class)).toString());
+        assertEquals("illegalArgument()", new MethodSignature(Echo.class.getMethod("illegalArgument")).toString());
+        assertEquals("ioException()", new MethodSignature(Echo.class.getMethod("ioException")).toString());
+    }
+
+    @Test
+    public void testToMethod() throws Exception
+    {
+        final MethodSignature sig = new MethodSignature(Echo.class.getMethod("echoBack", String.class));
+
+        assertMethodIs(sig.toMethod(Echo.class), Echo.class, "echoBack", String.class);
+        assertMethodIs(sig.toMethod(AbstractEcho.class), AbstractEcho.class, "echoBack", String.class);
+        assertMethodIs(sig.toMethod(EchoImpl.class), AbstractEcho.class, "echoBack", String.class);
+        assertMethodIs(sig.toMethod(DuplicateEcho.class), DuplicateEcho.class, "echoBack", String.class);
+    }
+
+    private void assertMethodIs(Method method, Class<?> declaredBy, String name, Class<?>... parameterTypes)
+    {
+        assertEquals(declaredBy, method.getDeclaringClass());
+        assertEquals(name, method.getName());
+        assertArrayEquals(parameterTypes, method.getParameterTypes());
     }
 }
\ No newline at end of file
diff --git a/core/src/test/java/org/apache/commons/proxy2/util/Echo.java b/core/src/test/java/org/apache/commons/proxy2/util/Echo.java
index 08749d7..af04e06 100644
--- a/core/src/test/java/org/apache/commons/proxy2/util/Echo.java
+++ b/core/src/test/java/org/apache/commons/proxy2/util/Echo.java
@@ -35,6 +35,10 @@
 

     public String echoBack( String[] messages );

 

+    public String echoBack( String[][] messages );

+

+    public String echoBack( String[][][] messages );

+

     public int echoBack( int i );

 

     public boolean echoBack( boolean b );

@@ -44,4 +48,5 @@
     public void illegalArgument();

 

     public void ioException() throws IOException;

+

 }

diff --git a/core/src/test/java/org/apache/commons/proxy2/util/EchoImpl.java b/core/src/test/java/org/apache/commons/proxy2/util/EchoImpl.java
index 21b0b7b..66169d5 100644
--- a/core/src/test/java/org/apache/commons/proxy2/util/EchoImpl.java
+++ b/core/src/test/java/org/apache/commons/proxy2/util/EchoImpl.java
@@ -48,7 +48,7 @@
 

     public String echoBack( String[] messages )

     {

-        final StringBuffer sb = new StringBuffer();

+        final StringBuilder sb = new StringBuilder();

         for( int i = 0; i < messages.length; i++ )

         {

             String message = messages[i];

@@ -57,6 +57,26 @@
         return sb.toString();

     }

 

+    public String echoBack( String[][] messages )

+    {

+        final StringBuilder sb = new StringBuilder();

+        for( int i = 0; i < messages.length; i++ )

+        {

+            sb.append(echoBack(messages[i]));

+        }

+        return sb.toString();

+    }

+

+    public String echoBack( String[][][] messages )

+    {

+        final StringBuilder sb = new StringBuilder();

+        for( int i = 0; i < messages.length; i++ )

+        {

+            sb.append(echoBack(messages[i]));

+        }

+        return sb.toString();

+    }

+

     public int echoBack( int i )

     {

         return i;