changeset 533:2d5107512e57

Automate JavaBean to Chunk conversions In an effort to reduce boilerplate code that converts our data objects to Chunks and back, this patch introduces annotations and helpers to automatically convert bean-like objects into Chunks. The ChunkAdapterTest class has many examples on how to use it. Reviewed-by: neugens Review-thread: http://icedtea.classpath.org/pipermail/thermostat/2012-August/002672.html
author Omair Majid <omajid@redhat.com>
date Thu, 16 Aug 2012 14:20:17 -0400
parents 3831389cc3b4
children 5943d72a26b5
files common/core/pom.xml common/core/src/main/java/com/redhat/thermostat/common/storage/Categories.java common/core/src/main/java/com/redhat/thermostat/common/storage/Category.java common/core/src/main/java/com/redhat/thermostat/common/storage/Chunk.java common/core/src/main/java/com/redhat/thermostat/common/storage/ChunkAdapter.java common/core/src/main/java/com/redhat/thermostat/common/storage/Entity.java common/core/src/main/java/com/redhat/thermostat/common/storage/Persist.java common/core/src/test/java/com/redhat/thermostat/common/storage/ChunkAdapterTest.java distribution/config/osgi-export.properties pom.xml
diffstat 10 files changed, 649 insertions(+), 34 deletions(-) [+]
line wrap: on
line diff
--- a/common/core/pom.xml	Thu Aug 16 14:45:09 2012 +0200
+++ b/common/core/pom.xml	Thu Aug 16 14:20:17 2012 -0400
@@ -134,6 +134,10 @@
       <artifactId>mongo-java-driver</artifactId>
     </dependency>
     <dependency>
+      <groupId>commons-beanutils</groupId>
+      <artifactId>commons-beanutils</artifactId>
+    </dependency>
+    <dependency>
       <groupId>commons-cli</groupId>
       <artifactId>commons-cli</artifactId>
     </dependency>
@@ -164,9 +168,6 @@
     	<artifactId>thermostat-keyring</artifactId>
     	<version>${project.version}</version>
     </dependency>
-    <dependency><groupId>org.osgi</groupId>
-      <artifactId>org.osgi.compendium</artifactId>
-    </dependency>
   </dependencies>
 
   <properties>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/common/core/src/main/java/com/redhat/thermostat/common/storage/Categories.java	Thu Aug 16 14:20:17 2012 -0400
@@ -0,0 +1,61 @@
+/*
+ * Copyright 2012 Red Hat, Inc.
+ *
+ * This file is part of Thermostat.
+ *
+ * Thermostat is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published
+ * by the Free Software Foundation; either version 2, or (at your
+ * option) any later version.
+ *
+ * Thermostat is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Thermostat; see the file COPYING.  If not see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * Linking this code with other modules is making a combined work
+ * based on this code.  Thus, the terms and conditions of the GNU
+ * General Public License cover the whole combination.
+ *
+ * As a special exception, the copyright holders of this code give
+ * you permission to link this code with independent modules to
+ * produce an executable, regardless of the license terms of these
+ * independent modules, and to copy and distribute the resulting
+ * executable under terms of your choice, provided that you also
+ * meet, for each linked independent module, the terms and conditions
+ * of the license of that module.  An independent module is a module
+ * which is not derived from or based on this code.  If you modify
+ * this code, you may extend this exception to your version of the
+ * library, but you are not obligated to do so.  If you do not wish
+ * to do so, delete this exception statement from your version.
+ */
+
+package com.redhat.thermostat.common.storage;
+
+import java.util.HashMap;
+import java.util.Map;
+
+public class Categories {
+
+    private static final Map<String, Category> namesToCategories = new HashMap<>();
+
+    public static synchronized boolean contains(String name) {
+        return namesToCategories.containsKey(name);
+    }
+
+    public static synchronized void add(Category category) {
+        if (namesToCategories.containsKey(category.getName())) {
+            throw new IllegalArgumentException("category already registered");
+        }
+        namesToCategories.put(category.getName(), category);
+    }
+
+    public static synchronized Category getByName(String categoryName) {
+        return namesToCategories.get(categoryName);
+    }
+
+}
--- a/common/core/src/main/java/com/redhat/thermostat/common/storage/Category.java	Thu Aug 16 14:45:09 2012 +0200
+++ b/common/core/src/main/java/com/redhat/thermostat/common/storage/Category.java	Thu Aug 16 14:20:17 2012 -0400
@@ -39,9 +39,7 @@
 import java.util.Collection;
 import java.util.Collections;
 import java.util.HashMap;
-import java.util.HashSet;
 import java.util.Map;
-import java.util.Set;
 
 public class Category {
     private final String name;
@@ -49,8 +47,6 @@
 
     private ConnectionKey connectionKey;
 
-    private static Set<String> categoryNames = new HashSet<String>();
-
     /**
      * Creates a new Category instance with the specified name.
      *
@@ -59,16 +55,16 @@
      * @throws IllegalArgumentException if a Category is created with a name that has been used before
      */
     public Category(String name, Key<?>... keys) {
-        if (categoryNames.contains(name)) {
+        if (Categories.contains(name)) {
             throw new IllegalStateException();
         }
-        categoryNames.add(name);
         this.name = name;
         Map<String, Key<?>> keysMap = new HashMap<String, Key<?>>();
         for (Key<?> key : keys) {
             keysMap.put(key.getName(), key);
         }
         this.keys = Collections.unmodifiableMap(keysMap);
+        Categories.add(this);
     }
 
     public String getName() {
--- a/common/core/src/main/java/com/redhat/thermostat/common/storage/Chunk.java	Thu Aug 16 14:45:09 2012 +0200
+++ b/common/core/src/main/java/com/redhat/thermostat/common/storage/Chunk.java	Thu Aug 16 14:20:17 2012 -0400
@@ -38,7 +38,6 @@
 
 import java.util.LinkedHashMap;
 import java.util.Map;
-import java.util.Map.Entry;
 import java.util.Objects;
 import java.util.Set;
 
@@ -47,11 +46,16 @@
  * that exists behind the storage layer.
  */
 public class Chunk {
-    private final Category category;
     private final boolean replace;
 
+    protected Category category;
     private Map<Key<?>, Object> values = new LinkedHashMap<Key<?>, Object>();
 
+    protected Chunk() {
+        // FIXME this replace is wrong. it depends on the type of data we are interested in
+        replace = false;
+    }
+
     /**
      *
      * @param category The {@link Category} of this data.  This should be a Category that the {@link Backend}
@@ -60,6 +64,7 @@
      * or be added to a set of values in this category
      */
     public Chunk(Category category, boolean replace) {
+        // FIXME the insertion behaviour should not be part of the data structure itself
         this.category = category;
         this.replace = replace;
     }
@@ -92,29 +97,7 @@
             return false;
         }
         Chunk other = (Chunk) o;
-        return equalCategory(other) && equalValues(other);
-
-    }
-
-    private boolean equalCategory(Chunk other) {
-        return category == other.category;
-    }
-
-    private boolean equalValues(Chunk other) {
-        if (values.size() != other.values.size()) {
-            return false;
-        }
-        for (Entry<Key<?>, Object> entry : values.entrySet()) {
-            if (! equalEntry(other, entry)) {
-                return false;
-            }
-        }
-        return true;
-    }
-
-    private boolean equalEntry(Chunk other, Entry<Key<?>, Object> entry) {
-        Key<?> key = entry.getKey();
-        return other.values.containsKey(key) && Objects.equals(other.values.get(key), values.get(key));
+        return Objects.equals(this.category, other.category) && Objects.equals(this.values, other.values);
     }
 
 }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/common/core/src/main/java/com/redhat/thermostat/common/storage/ChunkAdapter.java	Thu Aug 16 14:20:17 2012 -0400
@@ -0,0 +1,184 @@
+/*
+ * Copyright 2012 Red Hat, Inc.
+ *
+ * This file is part of Thermostat.
+ *
+ * Thermostat is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published
+ * by the Free Software Foundation; either version 2, or (at your
+ * option) any later version.
+ *
+ * Thermostat is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Thermostat; see the file COPYING.  If not see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * Linking this code with other modules is making a combined work
+ * based on this code.  Thus, the terms and conditions of the GNU
+ * General Public License cover the whole combination.
+ *
+ * As a special exception, the copyright holders of this code give
+ * you permission to link this code with independent modules to
+ * produce an executable, regardless of the license terms of these
+ * independent modules, and to copy and distribute the resulting
+ * executable under terms of your choice, provided that you also
+ * meet, for each linked independent module, the terms and conditions
+ * of the license of that module.  An independent module is a module
+ * which is not derived from or based on this code.  If you modify
+ * this code, you may extend this exception to your version of the
+ * library, but you are not obligated to do so.  If you do not wish
+ * to do so, delete this exception statement from your version.
+ */
+
+package com.redhat.thermostat.common.storage;
+
+import java.beans.PropertyDescriptor;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.util.HashSet;
+import java.util.Set;
+
+import org.apache.commons.beanutils.BeanUtils;
+import org.apache.commons.beanutils.PropertyUtils;
+
+/**
+ * Adapts a bean into a Chunk. The bean must be annotated with {@link Entity}.
+ * All methods to persist must be annotated with {@link Persist}
+ */
+public class ChunkAdapter extends Chunk {
+
+    private final Object adaptee;
+
+    public ChunkAdapter(Object obj) {
+        super();
+
+        checkForAnnotation(obj);
+        Set<Key<?>> keys = identifyKeys(obj);
+        category = createCategory(obj, keys);
+        adaptee = obj;
+    }
+
+    private void checkForAnnotation(Object toCheck) {
+        if (!toCheck.getClass().isAnnotationPresent(Entity.class)) {
+            throw new IllegalArgumentException("object to adapt must be annotated with Entity");
+        }
+    }
+
+    private Set<Key<?>> identifyKeys(Object obj) {
+        Set<Key<?>> keys = new HashSet<>();
+
+        PropertyDescriptor[] descriptors = PropertyUtils.getPropertyDescriptors(obj);
+        for (PropertyDescriptor descriptor : descriptors) {
+            if (hasValidGetAndSetMethods(descriptor)) {
+                // FIXME this is sometimes a partial key
+                Key<?> key = new Key<>(findKeyName(descriptor), false);
+                keys.add(key);
+            }
+        }
+        return keys;
+    }
+
+    private boolean hasValidGetAndSetMethods(PropertyDescriptor descriptor) {
+        Method readMethod = descriptor.getReadMethod();
+        Method writeMethod = descriptor.getWriteMethod();
+
+        if (readMethod != null && writeMethod != null) {
+            if (readMethod.isAnnotationPresent(Persist.class) &&
+                    writeMethod.isAnnotationPresent(Persist.class)) {
+                return true;
+            } else if (readMethod.isAnnotationPresent(Persist.class) ^
+                    writeMethod.isAnnotationPresent(Persist.class)) {
+                throw new IllegalArgumentException("annotation only present on one of get/set method");
+            }
+        }
+        return false;
+    }
+
+    private String findKeyName(PropertyDescriptor descriptor) {
+        final String NAME_UNSPECIFIED = "";
+
+        String computedName = descriptor.getName();
+        String nameOnGetMethod = descriptor.getReadMethod().getAnnotation(Persist.class).name();
+        String nameOnSetMethod = descriptor.getWriteMethod().getAnnotation(Persist.class).name();
+
+        String attributeName;
+        if (nameOnGetMethod.equals(NAME_UNSPECIFIED) && nameOnSetMethod.equals(NAME_UNSPECIFIED)) {
+            attributeName = computedName;
+        } else {
+            if (!nameOnGetMethod.equals(nameOnSetMethod) && nameOnSetMethod.equals(NAME_UNSPECIFIED)) {
+                attributeName = nameOnGetMethod;
+            } else if (!nameOnSetMethod.equals(nameOnGetMethod) && nameOnGetMethod.equals(NAME_UNSPECIFIED)) {
+                attributeName = nameOnSetMethod;
+            } else {
+                throw new IllegalArgumentException("set/get methods have mismatching names in annotation");
+            }
+        }
+
+        return attributeName;
+    }
+
+    private Category createCategory(Object obj, Set<Key<?>> keys) {
+        String newCategoryName = findCategoryName(obj);
+        Category category;
+        if (Categories.contains(newCategoryName)) {
+            Set<Key<?>> existingKeys= new HashSet<>(Categories.getByName(newCategoryName).getKeys());
+            if (!keys.equals(existingKeys)) {
+                throw new IllegalArgumentException("this class, with a differet organization was seen previously");
+            }
+            category = Categories.getByName(newCategoryName);
+        } else {
+            category = new Category(newCategoryName, keys.toArray(new Key<?>[0]));
+        }
+        return category;
+    }
+
+    private String findCategoryName(Object obj) {
+        Entity categoryAnnotation = obj.getClass().getAnnotation(Entity.class);
+        String desiredName = categoryAnnotation.name();
+        if (desiredName.equals("")) {
+            desiredName = obj.getClass().getSimpleName();
+        }
+        return desiredName;
+    }
+
+    @Override
+    public <T> T get(Key<T> entry) {
+        try {
+            return (T) PropertyUtils.getProperty(adaptee, entry.getName());
+        } catch (IllegalAccessException | InvocationTargetException | NoSuchMethodException e) {
+            e.printStackTrace();
+            try {
+                System.err.println(BeanUtils.describe(adaptee));
+            } catch (IllegalAccessException | InvocationTargetException | NoSuchMethodException e1) {
+                e1.printStackTrace();
+            }
+        }
+        return null;
+    }
+
+    @Override
+    public <T> void put(Key<T> entry, T value) {
+        String keyName = entry.getName();
+        try {
+            BeanUtils.setProperty(adaptee, keyName, value);
+        } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) {
+            e.printStackTrace();
+            try {
+                System.err.println(BeanUtils.describe(adaptee));
+            } catch (IllegalAccessException | InvocationTargetException | NoSuchMethodException e1) {
+                e1.printStackTrace();
+            }
+        }
+    }
+
+    @Override
+    public Set<Key<?>> getKeys() {
+        Category category = getCategory();
+        return new HashSet<>(category.getKeys());
+    }
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/common/core/src/main/java/com/redhat/thermostat/common/storage/Entity.java	Thu Aug 16 14:20:17 2012 -0400
@@ -0,0 +1,50 @@
+/*
+ * Copyright 2012 Red Hat, Inc.
+ *
+ * This file is part of Thermostat.
+ *
+ * Thermostat is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published
+ * by the Free Software Foundation; either version 2, or (at your
+ * option) any later version.
+ *
+ * Thermostat is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Thermostat; see the file COPYING.  If not see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * Linking this code with other modules is making a combined work
+ * based on this code.  Thus, the terms and conditions of the GNU
+ * General Public License cover the whole combination.
+ *
+ * As a special exception, the copyright holders of this code give
+ * you permission to link this code with independent modules to
+ * produce an executable, regardless of the license terms of these
+ * independent modules, and to copy and distribute the resulting
+ * executable under terms of your choice, provided that you also
+ * meet, for each linked independent module, the terms and conditions
+ * of the license of that module.  An independent module is a module
+ * which is not derived from or based on this code.  If you modify
+ * this code, you may extend this exception to your version of the
+ * library, but you are not obligated to do so.  If you do not wish
+ * to do so, delete this exception statement from your version.
+ */
+
+package com.redhat.thermostat.common.storage;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.TYPE)
+public @interface Entity {
+
+    public String name() default "";
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/common/core/src/main/java/com/redhat/thermostat/common/storage/Persist.java	Thu Aug 16 14:20:17 2012 -0400
@@ -0,0 +1,48 @@
+/*
+ * Copyright 2012 Red Hat, Inc.
+ *
+ * This file is part of Thermostat.
+ *
+ * Thermostat is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published
+ * by the Free Software Foundation; either version 2, or (at your
+ * option) any later version.
+ *
+ * Thermostat is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Thermostat; see the file COPYING.  If not see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * Linking this code with other modules is making a combined work
+ * based on this code.  Thus, the terms and conditions of the GNU
+ * General Public License cover the whole combination.
+ *
+ * As a special exception, the copyright holders of this code give
+ * you permission to link this code with independent modules to
+ * produce an executable, regardless of the license terms of these
+ * independent modules, and to copy and distribute the resulting
+ * executable under terms of your choice, provided that you also
+ * meet, for each linked independent module, the terms and conditions
+ * of the license of that module.  An independent module is a module
+ * which is not derived from or based on this code.  If you modify
+ * this code, you may extend this exception to your version of the
+ * library, but you are not obligated to do so.  If you do not wish
+ * to do so, delete this exception statement from your version.
+ */
+
+package com.redhat.thermostat.common.storage;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.METHOD)
+public @interface Persist {
+    public String name() default "";
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/common/core/src/test/java/com/redhat/thermostat/common/storage/ChunkAdapterTest.java	Thu Aug 16 14:20:17 2012 -0400
@@ -0,0 +1,285 @@
+/*
+ * Copyright 2012 Red Hat, Inc.
+ *
+ * This file is part of Thermostat.
+ *
+ * Thermostat is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published
+ * by the Free Software Foundation; either version 2, or (at your
+ * option) any later version.
+ *
+ * Thermostat is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Thermostat; see the file COPYING.  If not see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * Linking this code with other modules is making a combined work
+ * based on this code.  Thus, the terms and conditions of the GNU
+ * General Public License cover the whole combination.
+ *
+ * As a special exception, the copyright holders of this code give
+ * you permission to link this code with independent modules to
+ * produce an executable, regardless of the license terms of these
+ * independent modules, and to copy and distribute the resulting
+ * executable under terms of your choice, provided that you also
+ * meet, for each linked independent module, the terms and conditions
+ * of the license of that module.  An independent module is a module
+ * which is not derived from or based on this code.  If you modify
+ * this code, you may extend this exception to your version of the
+ * library, but you are not obligated to do so.  If you do not wish
+ * to do so, delete this exception statement from your version.
+ */
+package com.redhat.thermostat.common.storage;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertTrue;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Set;
+
+import org.junit.Test;
+
+public class ChunkAdapterTest {
+
+    @Entity
+    public static class SomeData {
+
+        private long[] arrayData;
+        private List<String> listData;
+        private long longData;
+        private String stringData;
+
+        @Persist
+        public long[] getArrayData() {
+            return arrayData;
+        }
+
+        @Persist
+        public void setArrayData(long[] arrayData) {
+            this.arrayData = arrayData;
+        }
+
+        public void setListData(List<String> data) {
+            this.listData = data;
+        }
+
+        public List<String> getListData() {
+            return listData;
+        }
+
+        public long getLongData() {
+            return longData;
+        }
+
+        public void setLongData(long newValue) {
+            longData = newValue;
+        }
+
+        public void setStringData(String newStringData) {
+            stringData = newStringData;
+        }
+
+        public String getStringData() {
+            return stringData;
+        }
+
+    }
+
+    // the expected keys for each 'property' in the SomeData bean
+    private static final Key<long[]> arrayData = new Key<>("arrayData", false);
+    private static final Key<List<String>> listData = new Key<>("listData", false);
+    private static final Key<Long> longData = new Key<>("longData", false);
+    private static final Key<String> stringData = new Key<>("stringData", false);
+
+    @Test
+    public void verifyAdapaterCanBeUsedInPlaceOfChunk() {
+        SomeData testObject = new SomeData();
+        ChunkAdapter adapter = new ChunkAdapter(testObject);
+
+        assertTrue(adapter instanceof Chunk);
+    }
+
+    @Test
+    public void verifyCategoryName() {
+        SomeData testObject = new SomeData();
+        ChunkAdapter adapter = new ChunkAdapter(testObject);
+        Category category = adapter.getCategory();
+        assertNotNull(category);
+        assertEquals("SomeData", category.getName());
+    }
+
+    @Test
+    public void verifyKeys() throws InterruptedException {
+        SomeData testObject = new SomeData();
+        Chunk chunk = new ChunkAdapter(testObject);
+        testObject.setArrayData(new long[] { 0xADD });
+
+        Set<Key<?>> recognizedKeys = chunk.getKeys();
+        // only one method is annotated with @Persist
+        assertEquals(1, recognizedKeys.size());
+        // verify that single key can be read
+        Key<?> onlyRecognizedKey = recognizedKeys.iterator().next();
+        assertArrayEquals(new long[] { 0xADD }, (long[]) chunk.get(onlyRecognizedKey));
+    }
+
+    @Test
+    public void verifyGetAndPutLongValue() {
+        SomeData testObject = new SomeData();
+        Chunk chunk = new ChunkAdapter(testObject);
+        testObject.setLongData(1l);
+
+        assertEquals((Long) 1l, chunk.get(longData));
+
+        chunk.put(longData, 2l);
+
+        assertEquals((Long) 2l, chunk.get(longData));
+        assertEquals(2, testObject.longData);
+    }
+
+    @Test
+    public void verifyGetAndPutStringValue() {
+        SomeData testObject = new SomeData();
+        Chunk chunk = new ChunkAdapter(testObject);
+        testObject.setStringData("stringData");
+
+        assertEquals("stringData", chunk.get(stringData));
+
+        chunk.put(stringData, "some-new-data");
+
+        assertEquals("some-new-data", chunk.get(stringData));
+        assertEquals("some-new-data", testObject.stringData);
+    }
+
+    @Test
+    public void verifyGetAndPutArrayValue() {
+        SomeData testObject = new SomeData();
+        Chunk chunk = new ChunkAdapter(testObject);
+        testObject.setArrayData(new long[] { 0xADD } );
+
+        assertArrayEquals(new long[] { 0xADD }, chunk.get(arrayData));
+
+        chunk.put(arrayData, new long[] { 0xC0FFEE });
+
+        assertArrayEquals(new long[] { 0xC0FFEE }, chunk.get(arrayData));
+        assertArrayEquals(new long[] { 0xC0FFEE }, testObject.arrayData);
+    }
+
+    @Test
+    public void verifyGetAndPutListValue() {
+        SomeData testObject = new SomeData();
+        Chunk chunk = new ChunkAdapter(testObject);
+        List<String> newList = new ArrayList<>();
+        testObject.setListData(newList);
+
+        assertEquals(newList, chunk.get(listData));
+
+        chunk.put(listData, Collections.<String>emptyList());
+
+        assertSame(Collections.<String>emptyList(), chunk.get(listData));
+        assertSame(Collections.<String>emptyList(), testObject.listData);
+    }
+
+    @Test (expected=IllegalArgumentException.class)
+    public void verifyNonEntityIsNotAcceptable() {
+        // missing @Entity annotation
+        class NonEntity {
+            public void setInteger(int integer) { /* no op */ }
+            public int getInteger() { return 42; }
+        }
+
+        NonEntity toAdapt = new NonEntity();
+        new ChunkAdapter(toAdapt);
+    }
+
+    @Test (expected=IllegalArgumentException.class)
+    public void verifyMissingPersistAnnotationOnOneMethodThrowsException() {
+        @Entity
+        class DataWithMissingAnnotationOnOneMethod {
+            @Persist public void setInteger(int integer) { /* no op */ }
+            public int getInteger() { return 42; }
+        }
+
+        DataWithMissingAnnotationOnOneMethod toAdapt = new DataWithMissingAnnotationOnOneMethod();
+        new ChunkAdapter(toAdapt);
+    }
+
+    @Test
+    public void verifyCustomEntityNameIsCategoryName() {
+        final String ENTITY_NAME = "custom-data-name";
+
+        @Entity (name=ENTITY_NAME)
+        class DataWithCustomName {
+            public void setData(String data) { /* no-op */ }
+            public String getData() { return "ignore this value" ; }
+        }
+
+        Chunk chunk = new ChunkAdapter(new DataWithCustomName());
+
+        assertEquals(ENTITY_NAME, chunk.getCategory().getName());
+    }
+
+    @Test
+    public void verifyCustomAttributeNameOnGetIsKeyName() {
+        final String ATTRIBUTE_NAME = "custom-attribute";
+        Key<?> expectedKey = new Key<>(ATTRIBUTE_NAME, false);
+
+        @Entity
+        class DataWithCustomAttributeNameOnGet {
+            @Persist (name=ATTRIBUTE_NAME)
+            public String getCustomAttribute() { return "ignore this value" ; }
+            @Persist
+            public void setCustomAttribute(String attribute) { /* no op */ }
+        }
+
+        Chunk chunk = new ChunkAdapter(new DataWithCustomAttributeNameOnGet());
+
+        assertTrue(chunk.getKeys().contains(expectedKey));
+
+        assertTrue(chunk.getCategory().getKeys().contains(expectedKey));
+        assertNotNull(chunk.getCategory().getKey(ATTRIBUTE_NAME));
+    }
+
+    @Test
+    public void verifyCustomAttributeNameOnSetIsKeyName() {
+        final String ATTRIBUTE_NAME = "custom-attribute";
+        Key<?> expectedKey = new Key<>(ATTRIBUTE_NAME, false);
+
+        @Entity
+        class DataWithCustomAttributeNameOnSet {
+            @Persist
+            public String getCustomAttribute() { return "ignore this value" ; }
+            @Persist (name=ATTRIBUTE_NAME)
+            public void setCustomAttribute(String attribute) { /* no op */ }
+        }
+
+        Chunk chunk = new ChunkAdapter(new DataWithCustomAttributeNameOnSet());
+
+        assertTrue(chunk.getKeys().contains(expectedKey));
+
+        assertTrue(chunk.getCategory().getKeys().contains(expectedKey));
+        assertNotNull(chunk.getCategory().getKey(ATTRIBUTE_NAME));
+    }
+
+    @Test (expected=IllegalArgumentException.class)
+    public void verifyExceptionOnCustomAttributeNameMismatch() {
+        @Entity
+        class DataWithCustomAttributeNameMismatch {
+            @Persist (name="one-name")
+            public String getCustomAttribute() { return "ignore this value" ; }
+            @Persist (name="different-name")
+            public void setCustomAttribute(String attribute) { /* no op */ }
+        }
+
+        new ChunkAdapter(new DataWithCustomAttributeNameMismatch());
+    }
+
+}
--- a/distribution/config/osgi-export.properties	Thu Aug 16 14:45:09 2012 +0200
+++ b/distribution/config/osgi-export.properties	Thu Aug 16 14:20:17 2012 -0400
@@ -36,6 +36,7 @@
 com.mongodb
 com.mongodb.gridfs
 org.apache.commons.cli=1.2.0
+org.apache.commons.beanutils=1.8.3
 org.bson
 org.bson.types
 org.jfree.chart
--- a/pom.xml	Thu Aug 16 14:45:09 2012 +0200
+++ b/pom.xml	Thu Aug 16 14:20:17 2012 -0400
@@ -70,6 +70,7 @@
     <jdktools.version>1.7.0</jdktools.version>
     <jfreechart.version>1.0.14</jfreechart.version>
     <mongo-driver.version>2.7.3</mongo-driver.version>
+    <commons-beanutils.version>1.8.3</commons-beanutils.version>
     <commons-cli.version>1.2</commons-cli.version>
     <jline.version>2.5</jline.version>
     <lucene.version>3.6.0</lucene.version>
@@ -227,6 +228,11 @@
         <version>${mongo-driver.version}</version>
       </dependency>
       <dependency>
+        <groupId>commons-beanutils</groupId>
+        <artifactId>commons-beanutils</artifactId>
+        <version>${commons-beanutils.version}</version>
+      </dependency>
+      <dependency>
         <groupId>commons-cli</groupId>
         <artifactId>commons-cli</artifactId>
         <version>${commons-cli.version}</version>