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
+ }
+ }
+}