Repository: orika-mapper/orika
Pull request #165 (2016-12-08 19:38:16)

Enhance RegexPropertyResolver to allow regex expressions with more than one capture group
Currently you cannot use a regex that has more than one capture group
because the RegexPropertyResolver assumes group(1) is the capture group
that will specify the property name.

In my code base I have the following use case:

1.  Some classes in the code base are immutable and you construct them
using a Builder class.
2. Some of the builder classes use a withXXX convention
3. Some of the builder classes use a setXXX convention
4. I needed a write method regex of “(with|set)(\\w+)” which broke when
the RegexPropertyResolver expected the group containing the property
name to have be index 1.

This change should be backwards compatible.


Patch:
diff --git a/core/src/main/java/ma/glasnost/orika/property/RegexPropertyResolver.java b/core/src/main/java/ma/glasnost/orika/property/RegexPropertyResolver.java
index 434b852f..0c94aa76 100644
--- a/core/src/main/java/ma/glasnost/orika/property/RegexPropertyResolver.java
+++ b/core/src/main/java/ma/glasnost/orika/property/RegexPropertyResolver.java
@@ -18,6 +18,10 @@
 
 package ma.glasnost.orika.property;
 
+import ma.glasnost.orika.metadata.Property;
+import ma.glasnost.orika.metadata.Type;
+import ma.glasnost.orika.metadata.TypeFactory;
+
 import java.lang.reflect.Method;
 import java.util.LinkedHashMap;
 import java.util.Map;
@@ -25,38 +29,36 @@ import java.util.Map.Entry;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 
-import ma.glasnost.orika.metadata.Property;
-import ma.glasnost.orika.metadata.Type;
-import ma.glasnost.orika.metadata.TypeFactory;
-
 /**
  * RegexPropertyResolver uses regular expressions to find properties based
  * on patterns configured for locating the read and write methods.<br><br>
- * 
- * The patterns provided should produce a match where group(1) returns the 
- * name of the property.<br>
+ * <p>
+ * The patterns provided should produce a match where group(1) returns the
+ * name of the property. If you need to specify a regex that contains more than one capture group, use the constructor
+ * that allows you to specify the capture group index that will return the name of the property.<br>
  * <em>Note that the name will automatically be un-capitalized, so you need not worry
  * about defining your regular expression to handle this.</em><br><br>
- * Only no-argument getter methods returning a type are considered for a read method match, 
+ * Only no-argument getter methods returning a type are considered for a read method match,
  * and only single-argument methods are considered for a write method match; the write method
  * need not have a void return type.<br><br>
  * The type of the setter method must be a sub-type (or matching type) of the getter method's type;
- * if the getter method is a strict sub-type, then the type of the setter method will define the 
+ * if the getter method is a strict sub-type, then the type of the setter method will define the
  * type of the property.
  * <br>
  * <h3>Example</h3><br> <strong>"read([\w]+)Property"</strong> would match a method named <strong>'readMySpecialProperty'</strong>,
  * and define the name of the corresponding property as 'MySpecial', which will be automatically un-capitalized
  * to <strong>'mySpecial'</strong>.<br><br>
- * 
- * @author matt.deboer@gmail.com
  *
+ * @author matt.deboer@gmail.com
  */
 public class RegexPropertyResolver extends IntrospectorPropertyResolver {
-    
+
     private final Pattern readPattern;
     private final Pattern writePattern;
     private final boolean includeJavaBeans;
-    
+    private int writeMethodRegexCaptureGroupIndex;
+    private int readMethodRegexCaptureGroupIndex;
+
     /**
      * @param readMethodRegex
      * @param writeMethodRegex
@@ -64,38 +66,45 @@ public class RegexPropertyResolver extends IntrospectorPropertyResolver {
      * @param includePublicFields
      */
     public RegexPropertyResolver(String readMethodRegex, String writeMethodRegex, boolean includeJavaBeans, boolean includePublicFields) {
+        this(readMethodRegex, writeMethodRegex, includeJavaBeans, includePublicFields, 1, 1);
+    }
+
+    public RegexPropertyResolver(String readMethodRegex, String writeMethodRegex, boolean includeJavaBeans, boolean includePublicFields, int readMethodRegexCaptureGroupIndex, int writeMethodRegexCaptureGroupIndex) {
         super(includePublicFields);
         this.includeJavaBeans = includeJavaBeans;
         this.readPattern = Pattern.compile(readMethodRegex);
         this.writePattern = Pattern.compile(writeMethodRegex);
+        this.readMethodRegexCaptureGroupIndex = readMethodRegexCaptureGroupIndex;
+        this.writeMethodRegexCaptureGroupIndex = writeMethodRegexCaptureGroupIndex;
     }
-    
+
     /**
      * Converts the first character of a String to lowercase
-     * 
+     *
      * @param string
      * @return the original String with the first character converted to lowercase
      */
     protected String uncapitalize(String string) {
         return string.substring(0, 1).toLowerCase() + string.substring(1, string.length());
     }
-    
+
     /**
      * Collects all properties for the specified type.
-     * 
-     * @param type the type for which to collect properties
+     *
+     * @param type          the type for which to collect properties
      * @param referenceType the reference type for use in resolving generic parameters as needed
-     * @param properties the properties collected for the current type
+     * @param properties    the properties collected for the current type
      */
     protected void collectProperties(Class<?> type, Type<?> referenceType, Map<String, Property> properties) {
-        
+
         Map<String, Property.Builder> collectedMethods = new LinkedHashMap<String, Property.Builder>();
-        for (Method m: type.getMethods()) {
-            
+        for (Method m : type.getMethods()) {
+
             if (m.getParameterTypes().length == 0 && m.getReturnType() != null && m.getReturnType() != Void.TYPE) {
                 Matcher readMatcher = readPattern.matcher(m.getName());
                 if (readMatcher.matches()) {
-                    String name = readMatcher.group(1);
+
+                    String name = readMatcher.group(readMethodRegexCaptureGroupIndex);
                     if (name != null) {
                         name = uncapitalize(name);
                         Property.Builder builder = collectedMethods.get(name);
@@ -105,15 +114,16 @@ public class RegexPropertyResolver extends IntrospectorPropertyResolver {
                         }
                         builder.getter(m);
                     } else {
-                        throw new IllegalStateException("the configured readMethod regex '" + readPattern + 
+                        throw new IllegalStateException("the configured readMethod regex '" + readPattern +
                                 "' does not define group (1) containing the property's name");
                     }
-                } 
+                }
             } else if (m.getParameterTypes().length == 1) {
-            
+
                 Matcher writeMatcher = writePattern.matcher(m.getName());
                 if (writeMatcher.matches()) {
-                    String name = writeMatcher.group(1);
+
+                    String name = writeMatcher.group(writeMethodRegexCaptureGroupIndex);
                     if (name != null) {
                         name = uncapitalize(name);
                         Property.Builder builder = collectedMethods.get(name);
@@ -123,18 +133,18 @@ public class RegexPropertyResolver extends IntrospectorPropertyResolver {
                         }
                         builder.setter(m);
                     } else {
-                        throw new IllegalStateException("the configured writeMethod regex '" + writePattern + 
+                        throw new IllegalStateException("the configured writeMethod regex '" + writePattern +
                                 "' does not define group (1) containing the property's name");
                     }
                 }
             }
         }
-           
-        for (Entry<String, Property.Builder> entry: collectedMethods.entrySet()) {
+
+        for (Entry<String, Property.Builder> entry : collectedMethods.entrySet()) {
             Property property = entry.getValue().build(this);
             processProperty(property.getName(), property.getType().getRawType(), entry.getValue().getReadMethod(), entry.getValue().getWriteMethod(), type, referenceType, properties);
         }
-        
+
         if (includeJavaBeans) {
             super.collectProperties(type, referenceType, properties);
         }


Test patch:
diff --git a/tests/src/main/java/ma/glasnost/orika/test/property/RegexPropertyResolverTestCase.java b/tests/src/main/java/ma/glasnost/orika/test/property/RegexPropertyResolverTestCase.java
index e28cd81c..b5e051be 100644
--- a/tests/src/main/java/ma/glasnost/orika/test/property/RegexPropertyResolverTestCase.java
+++ b/tests/src/main/java/ma/glasnost/orika/test/property/RegexPropertyResolverTestCase.java
@@ -17,7 +17,6 @@
  */
 package ma.glasnost.orika.test.property;
 
-import org.junit.Assert;
 import ma.glasnost.orika.MapperFacade;
 import ma.glasnost.orika.MapperFactory;
 import ma.glasnost.orika.impl.DefaultMapperFactory;
@@ -25,40 +24,41 @@ import ma.glasnost.orika.property.RegexPropertyResolver;
 import ma.glasnost.orika.test.property.TestCaseClasses.A;
 import ma.glasnost.orika.test.property.TestCaseClasses.Address;
 import ma.glasnost.orika.test.property.TestCaseClasses.B;
+import ma.glasnost.orika.test.property.TestCaseClasses.C;
+import ma.glasnost.orika.test.property.TestCaseClasses.D;
 import ma.glasnost.orika.test.property.TestCaseClasses.Name;
-
+import org.junit.Assert;
 import org.junit.Test;
 
 /**
  * @author matt.deboer@gmail.com
- *
  */
 public class RegexPropertyResolverTestCase {
-    
+
     @Test
     public void testRegexResolution() {
-        
-        MapperFactory factory = 
+
+        MapperFactory factory =
                 new DefaultMapperFactory.Builder()
-                    .propertyResolverStrategy(
-                            new RegexPropertyResolver(
-                                    "readThe([\\w]+)ForThisBean",
-                                    "assignThe([\\w]+)",
-                                    true, true))
-                    .build();
+                        .propertyResolverStrategy(
+                                new RegexPropertyResolver(
+                                        "readThe([\\w]+)ForThisBean",
+                                        "assignThe([\\w]+)",
+                                        true, true))
+                        .build();
         factory.registerClassMap(
                 factory.classMap(A.class, B.class)
-                    .field("name.firstName", "givenName")
-                    .field("name.lastName", "sirName")
-                    .field("address.city", "city")
-                    .field("address.street", "street")
-                    .field("address.postalCode", "postalCode")
-                    .field("address.country", "country")
-                );
-        
-        
+                        .field("name.firstName", "givenName")
+                        .field("name.lastName", "sirName")
+                        .field("address.city", "city")
+                        .field("address.street", "street")
+                        .field("address.postalCode", "postalCode")
+                        .field("address.country", "country")
+        );
+
+
         MapperFacade mapper = factory.getMapperFacade();
-        
+
         A a = new A();
         Name name = new Name();
         name.setFirstName("Albert");
@@ -70,15 +70,49 @@ public class RegexPropertyResolverTestCase {
         address.postalCode = "A1234FG";
         address.street = "1234 Easy St.";
         a.setAddress(address);
-        
-        
+
+
         B b = mapper.map(a, B.class);
-        
+
         Assert.assertNotNull(b);
-        
+
         A mapBack = mapper.map(b, A.class);
-        
+
         Assert.assertEquals(a, mapBack);
-        
+
+    }
+
+    @Test
+    public void testRegexResolutionWithSpecifiedCaptureGroupIndex() throws Exception {
+        MapperFactory factory =
+                new DefaultMapperFactory.Builder()
+                        .propertyResolverStrategy(
+                                new RegexPropertyResolver(
+                                        "(get)(\\w+)", //arbitrarily make the getter regex have two capture groups
+                                        "(set|with)(\\w+)", //C.Builder has withXXX methods and D.Builder has setXXX methods
+                                        true, true, 2, 2)) //our write method regex has two capture groups, index=2 captures the property name
+                        .build();
+
+        factory.registerClassMap(
+                factory.classMap(TestCaseClasses.C.class, D.Builder.class)
+                        .byDefault());
+
+        factory.registerClassMap(
+                factory.classMap(TestCaseClasses.D.class, C.Builder.class)
+                        .byDefault());
+
+
+        MapperFacade mapper = factory.getMapperFacade();
+
+        C c = new C.Builder()
+                .withFoo("hello")
+                .withBar(33)
+                .build();
+
+        D d = mapper.map(c, D.Builder.class).build();
+
+        C c1 = mapper.map(d, C.Builder.class).build();
+
+        Assert.assertEquals(c, c1);
     }
 }
diff --git a/tests/src/main/java/ma/glasnost/orika/test/property/TestCaseClasses.java b/tests/src/main/java/ma/glasnost/orika/test/property/TestCaseClasses.java
index ddbe4788..bed917d0 100644
--- a/tests/src/main/java/ma/glasnost/orika/test/property/TestCaseClasses.java
+++ b/tests/src/main/java/ma/glasnost/orika/test/property/TestCaseClasses.java
@@ -313,6 +313,130 @@ public interface TestCaseClasses {
         public String postalCode;
         public String country;
     }
+
+    public static class C {
+		private final String foo;
+		private final int bar;
+
+
+		private C(String foo, int bar) {
+			this.foo = foo;
+			this.bar = bar;
+		}
+
+		public String getFoo() {
+			return foo;
+		}
+
+		public int getBar() {
+			return bar;
+		}
+
+		@Override
+		public boolean equals(Object o) {
+			if (this == o) return true;
+			if (o == null || getClass() != o.getClass()) return false;
+
+			C c = (C) o;
+
+			if (bar != c.bar) return false;
+			return foo != null ? foo.equals(c.foo) : c.foo == null;
+
+		}
+
+		@Override
+		public int hashCode() {
+			int result = foo != null ? foo.hashCode() : 0;
+			result = 31 * result + bar;
+			return result;
+		}
+
+		public static class Builder {
+			private String foo;
+			private int bar;
+
+			public Builder() {
+			}
+
+			public Builder withFoo(String foo) {
+				this.foo = foo;
+				return this;
+			}
+
+			public Builder withBar(int bar) {
+				this.bar = bar;
+				return this;
+			}
+
+
+			public C build() {
+				return new C(foo, bar);
+			}
+		}
+	}
+
+	public static class D {
+		private final String foo;
+		private final int bar;
+
+
+		private D(String foo, int bar) {
+			this.foo = foo;
+			this.bar = bar;
+		}
+
+		public String getFoo() {
+			return foo;
+		}
+
+		public int getBar() {
+			return bar;
+		}
+
+		@Override
+		public boolean equals(Object o) {
+			if (this == o) return true;
+			if (o == null || getClass() != o.getClass()) return false;
+
+			D d = (D) o;
+
+			if (bar != d.bar) return false;
+			return foo != null ? foo.equals(d.foo) : d.foo == null;
+
+		}
+
+		@Override
+		public int hashCode() {
+			int result = foo != null ? foo.hashCode() : 0;
+			result = 31 * result + bar;
+			return result;
+		}
+
+		public static class Builder {
+			private String foo;
+			private int bar;
+
+			public Builder() {
+			}
+
+			public Builder setFoo(String foo) {
+				this.foo = foo;
+				return this;
+			}
+
+			public Builder setBar(int bar) {
+				this.bar = bar;
+				return this;
+			}
+
+
+			public D build() {
+				return new D(foo, bar);
+			}
+		}
+	}
+
+
     
     public static class Name {
         private String firstName;