Add utility class for copying Option values between objects.

Change-Id: Id97adad50cfc4abb03baa6197965a9af12abcb08
diff --git a/src/com/android/tradefed/config/OptionCopier.java b/src/com/android/tradefed/config/OptionCopier.java
new file mode 100644
index 0000000..e04b49f
--- /dev/null
+++ b/src/com/android/tradefed/config/OptionCopier.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tradefed.config;
+
+import java.lang.reflect.Field;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * A helper class that can copy {@link Option} field values with same names from one object to
+ * another.
+ */
+public class OptionCopier {
+
+    /**
+     * Copy the values from {@link Option} fields in <var>origObject</var> to <var>destObject</var>
+     *
+     * @param origObject the {@link Object} to copy from
+     * @param destObject the {@link Object} tp copy to
+     * @throws ConfigurationException if options failed to copy
+     */
+    public static void copyOptions(Object origObject, Object destObject)
+            throws ConfigurationException {
+        Collection<Field> origFields = OptionSetter.getOptionFieldsForClass(origObject.getClass());
+        Map<String, Field> destFieldMap = getFieldOptionMap(destObject);
+        for (Field origField : origFields) {
+            final Option option = origField.getAnnotation(Option.class);
+            Field destField = destFieldMap.remove(option.name());
+            if (destField != null) {
+                Object origValue = OptionSetter.getFieldValue(origField,
+                        origObject);
+                OptionSetter.setFieldValue(option.name(), destObject, destField, origValue);
+            }
+        }
+    }
+
+    /**
+     * Build a map of {@link Option#name()} to {@link Field} for given {@link Object}.
+     *
+     * @param destObject
+     * @return a {@link Map}
+     */
+    private static Map<String, Field> getFieldOptionMap(Object destObject) {
+        Collection<Field> destFields = OptionSetter.getOptionFieldsForClass(destObject.getClass());
+        Map<String, Field> fieldMap = new HashMap<String, Field>(destFields.size());
+        for (Field field : destFields) {
+            Option o = field.getAnnotation(Option.class);
+            fieldMap.put(o.name(), field);
+        }
+        return fieldMap;
+    }
+}
diff --git a/src/com/android/tradefed/config/OptionSetter.java b/src/com/android/tradefed/config/OptionSetter.java
index b9ff8be..c8456a2 100644
--- a/src/com/android/tradefed/config/OptionSetter.java
+++ b/src/com/android/tradefed/config/OptionSetter.java
@@ -241,7 +241,6 @@
      * @param valueText the value
      * @throws ConfigurationException if Option cannot be found or valueText is wrong type
      */
-    @SuppressWarnings("unchecked")
     public void setOptionValue(String optionName, String valueText) throws ConfigurationException {
         OptionFieldsForName optionFields = fieldsForArg(optionName);
         for (Map.Entry<Object, Field> fieldEntry : optionFields) {
@@ -256,24 +255,62 @@
                         String.format("Couldn't convert '%s' to a %s for option '%s'", valueText,
                                 type, optionName));
             }
-            try {
-                field.setAccessible(true);
-                if (Collection.class.isAssignableFrom(field.getType())) {
-                    Collection collection = (Collection)field.get(optionSource);
-                    if (collection == null) {
-                        throw new ConfigurationException(String.format(
-                                "internal error: no storage allocated for field '%s' (used for " +
-                                "option '%s') in class '%s'",
-                                field.getName(), optionName, optionSource.getClass().getName()));
-                    }
-                    collection.add(value);
-                } else {
-                    field.set(optionSource, value);
+            setFieldValue(optionName, optionSource, field, value);
+        }
+    }
+
+    /**
+     * Sets the given {@link Option} fields value.
+     *
+     * @param optionName the {@link Option#name()}
+     * @param optionSource the {@link Object} to set
+     * @param field the {@link Field}
+     * @param value the value to set
+     * @throws ConfigurationException
+     */
+    @SuppressWarnings("unchecked")
+    static void setFieldValue(String optionName, Object optionSource, Field field, Object value)
+            throws ConfigurationException {
+        try {
+            field.setAccessible(true);
+            if (Collection.class.isAssignableFrom(field.getType())) {
+                Collection collection = (Collection)field.get(optionSource);
+                if (collection == null) {
+                    throw new ConfigurationException(String.format(
+                            "internal error: no storage allocated for field '%s' (used for " +
+                            "option '%s') in class '%s'",
+                            field.getName(), optionName, optionSource.getClass().getName()));
                 }
-            } catch (IllegalAccessException e) {
-                throw new ConfigurationException(String.format(
-                        "internal error when setting option '%s'", optionName), e);
+                if (value instanceof Collection) {
+                    collection.addAll((Collection)value);
+                } else {
+                    collection.add(value);
+                }
+            } else if (Map.class.isAssignableFrom(field.getType())) {
+                Map map = (Map)field.get(optionSource);
+                if (map == null) {
+                    throw new ConfigurationException(String.format(
+                            "internal error: no storage allocated for field '%s' (used for " +
+                            "option '%s') in class '%s'",
+                            field.getName(), optionName, optionSource.getClass().getName()));
+                }
+                if (value instanceof Map) {
+                    map.putAll((Map)value);
+                } else {
+                    throw new ConfigurationException(String.format(
+                            "internal error: value provided for field '%s' is not a map (used " +
+                            "for option '%s') in class '%s'",
+                            field.getName(), optionName, optionSource.getClass().getName()));
+                }
+            } else {
+                field.set(optionSource, value);
             }
+        } catch (IllegalAccessException e) {
+            throw new ConfigurationException(String.format(
+                    "internal error when setting option '%s'", optionName), e);
+        } catch (IllegalArgumentException e) {
+            throw new ConfigurationException(String.format(
+                    "internal error when setting option '%s'", optionName), e);
         }
     }
 
@@ -463,7 +500,7 @@
      * @param optionClass the {@link Class} to search
      * @return a {@link Collection} of fields annotated with {@link Option}
      */
-    protected static Collection<Field> getOptionFieldsForClass(final Class<?> optionClass) {
+    static Collection<Field> getOptionFieldsForClass(final Class<?> optionClass) {
         Collection<Field> fieldList = new ArrayList<Field>();
         buildOptionFieldsForClass(optionClass, fieldList);
         return fieldList;
@@ -498,24 +535,35 @@
      *         empty (in case of {@link Collection}s
      */
     static String getFieldValueAsString(Field field, Object optionObject) {
-        try {
-            field.setAccessible(true);
-            Object fieldValue = field.get(optionObject);
-            if (fieldValue == null) {
+        Object fieldValue = getFieldValue(field, optionObject);
+        if (fieldValue == null) {
+            return null;
+        }
+        if (fieldValue instanceof Collection) {
+            Collection collection = (Collection)fieldValue;
+            if (collection.isEmpty()) {
                 return null;
             }
-            if (fieldValue instanceof Collection) {
-                Collection collection = (Collection)fieldValue;
-                if (collection.isEmpty()) {
-                    return null;
-                }
-            } else if (fieldValue instanceof Map) {
-                Map map = (Map)fieldValue;
-                if (map.isEmpty()) {
-                    return null;
-                }
+        } else if (fieldValue instanceof Map) {
+            Map map = (Map)fieldValue;
+            if (map.isEmpty()) {
+                return null;
             }
-            return fieldValue.toString();
+        }
+        return fieldValue.toString();
+    }
+
+    /**
+     * Return the given {@link Field}'s value, handling any exceptions.
+     *
+     * @param field the {@link Field}
+     * @param optionObject the {@link Object} to get field's value from.
+     * @return the field's value as a {@link Object}, or <code>null</code>
+     */
+    static Object getFieldValue(Field field, Object optionObject) {
+        try {
+            field.setAccessible(true);
+            return field.get(optionObject);
         } catch (IllegalArgumentException e) {
             Log.w(LOG_TAG, String.format(
                     "Could not read value for field %s in class %s. Reason: %s", field.getName(),
diff --git a/tests/src/com/android/tradefed/UnitTests.java b/tests/src/com/android/tradefed/UnitTests.java
index af614f7..98259a1 100644
--- a/tests/src/com/android/tradefed/UnitTests.java
+++ b/tests/src/com/android/tradefed/UnitTests.java
@@ -31,6 +31,7 @@
 import com.android.tradefed.config.ConfigurationFactoryTest;
 import com.android.tradefed.config.ConfigurationTest;
 import com.android.tradefed.config.ConfigurationXmlParserTest;
+import com.android.tradefed.config.OptionCopierTest;
 import com.android.tradefed.config.OptionSetterTest;
 import com.android.tradefed.device.DeviceManagerTest;
 import com.android.tradefed.device.DeviceSelectionOptionsTest;
@@ -118,6 +119,7 @@
         addTestSuite(ConfigurationFactoryTest.class);
         addTestSuite(ConfigurationTest.class);
         addTestSuite(ConfigurationXmlParserTest.class);
+        addTestSuite(OptionCopierTest.class);
         addTestSuite(OptionSetterTest.class);
 
         // device
diff --git a/tests/src/com/android/tradefed/config/OptionCopierTest.java b/tests/src/com/android/tradefed/config/OptionCopierTest.java
new file mode 100644
index 0000000..a2ef76b
--- /dev/null
+++ b/tests/src/com/android/tradefed/config/OptionCopierTest.java
@@ -0,0 +1,189 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tradefed.config;
+
+import junit.framework.TestCase;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Unit tests for {@link OptionCopier}.
+ */
+public class OptionCopierTest extends TestCase {
+
+    private static enum DefaultEnumClass {
+        VAL1, VAL3, VAL2;
+    }
+
+    private static class OptionSource {
+        @Option(name = "string", shortName = 's')
+        private String mMyOption = "value";
+
+        @Option(name = "int")
+        private int mMyIntOption = 42;
+
+        @SuppressWarnings("unused")
+        @Option(name = "notInDest")
+        private String mMyNotInDestOption = "foo";
+
+        @Option(name = "string_collection")
+        private Collection<String> mStringCollection = new ArrayList<String>();
+
+        @SuppressWarnings("unused")
+        @Option(name = "enum")
+        private DefaultEnumClass mEnum = DefaultEnumClass.VAL1;
+
+        @Option(name = "enum_map")
+        private Map<DefaultEnumClass, DefaultEnumClass> mEnumMap =
+                new HashMap<DefaultEnumClass, DefaultEnumClass>();
+
+        @Option(name = "enum_collection")
+        private Collection<DefaultEnumClass> mEnumCollection =
+                new ArrayList<DefaultEnumClass>();
+    }
+
+    /**
+     * Option source with an option with same name as OptionSource, but a different type.
+     */
+    private static class OptionWrongTypeDest {
+        @SuppressWarnings("unused")
+        @Option(name = "string", shortName = 's')
+        private int mMyOption;
+    }
+
+    /**
+     * Option source with an option with same name as OptionSource, but a different type.
+     */
+    @SuppressWarnings("unused")
+    private static class OptionWrongGenericTypeDest {
+
+        @Option(name = "string_collection")
+        private Collection<Integer> mIntCollection = new ArrayList<Integer>();
+
+    }
+
+    private static class OptionDest {
+
+        @Option(name = "string", shortName = 's')
+        private String mDestOption;
+
+        @Option(name = "int")
+        private int mDestIntOption;
+
+        @Option(name = "string_collection")
+        private Collection<String> mStringDestCollection = new ArrayList<String>();
+
+        @Option(name = "enum")
+        private DefaultEnumClass mEnum = null;
+
+        @Option(name = "enum_map")
+        private Map<DefaultEnumClass, DefaultEnumClass> mEnumMap =
+                new HashMap<DefaultEnumClass, DefaultEnumClass>();
+
+        @Option(name = "enum_collection")
+        private Collection<DefaultEnumClass> mEnumCollection =
+                new ArrayList<DefaultEnumClass>();
+    }
+
+    /**
+     * Test success case for {@link OptionCopier} using String fields
+     */
+    public void testCopyOptions_string() throws ConfigurationException {
+        OptionSource source = new OptionSource();
+        OptionDest dest = new OptionDest();
+        OptionCopier.copyOptions(source, dest);
+        assertEquals(source.mMyOption, dest.mDestOption);
+    }
+
+    /**
+     * Test success case for {@link OptionCopier} for an int field
+     */
+    public void testCopyOptions_int() throws ConfigurationException {
+        OptionSource source = new OptionSource();
+        OptionDest dest = new OptionDest();
+        OptionCopier.copyOptions(source, dest);
+        assertEquals(source.mMyIntOption, dest.mDestIntOption);
+    }
+
+    /**
+     * Test success case for {@link OptionCopier} for a {@link Collection}.
+     */
+    public void testCopyOptions_collection() throws ConfigurationException {
+        OptionSource source = new OptionSource();
+        source.mStringCollection.add("foo");
+        OptionDest dest = new OptionDest();
+        dest.mStringDestCollection.add("bar");
+        OptionCopier.copyOptions(source, dest);
+        assertEquals(2, dest.mStringDestCollection.size());
+        assertTrue(dest.mStringDestCollection.contains("foo"));
+        assertTrue(dest.mStringDestCollection.contains("bar"));
+    }
+
+    /**
+     * Test success case for {@link OptionCopier} for an enum field
+     */
+    public void testCopyOptions_enum() throws ConfigurationException {
+        OptionSource source = new OptionSource();
+        OptionDest dest = new OptionDest();
+        OptionCopier.copyOptions(source, dest);
+        assertEquals(DefaultEnumClass.VAL1, dest.mEnum);
+    }
+
+    /**
+     * Test success case for {@link OptionCopier} for an enum {@link Collection}.
+     */
+    public void testCopyOptions_enumCollection() throws ConfigurationException {
+        OptionSource source = new OptionSource();
+        source.mEnumCollection.add(DefaultEnumClass.VAL2);
+        OptionDest dest = new OptionDest();
+        source.mEnumCollection.add(DefaultEnumClass.VAL3);
+        OptionCopier.copyOptions(source, dest);
+        assertEquals(2, dest.mEnumCollection.size());
+        assertTrue(dest.mEnumCollection.contains(DefaultEnumClass.VAL2));
+        assertTrue(dest.mEnumCollection.contains(DefaultEnumClass.VAL3));
+    }
+
+    /**
+     * Test success case for {@link OptionCopier} for an enum {@link Map}.
+     */
+    public void testCopyOptions_enumMap() throws ConfigurationException {
+        OptionSource source = new OptionSource();
+        source.mEnumMap.put(DefaultEnumClass.VAL1, DefaultEnumClass.VAL2);
+        OptionDest dest = new OptionDest();
+        dest.mEnumMap.put(DefaultEnumClass.VAL2, DefaultEnumClass.VAL3);
+        OptionCopier.copyOptions(source, dest);
+        assertEquals(2, dest.mEnumMap.size());
+        assertEquals(DefaultEnumClass.VAL2, dest.mEnumMap.get(DefaultEnumClass.VAL1));
+    }
+
+    /**
+     * Test {@link OptionCopier} when field's to be copied have different types
+     */
+    public void testCopyOptions_wrongType() throws ConfigurationException {
+        OptionSource source = new OptionSource();
+        OptionWrongTypeDest dest = new OptionWrongTypeDest();
+        try {
+            OptionCopier.copyOptions(source, dest);
+            fail("ConfigurationException not thrown");
+        } catch (ConfigurationException e) {
+            // expected
+        }
+    }
+}