diff --git a/openjpa-persistence-jdbc/src/test/java/org/apache/openjpa/persistence/proxy/TestProxyCollection.java b/openjpa-persistence-jdbc/src/test/java/org/apache/openjpa/persistence/proxy/TestProxyCollection.java
new file mode 100644
index 000000000..1e7c8c098
--- /dev/null
+++ b/openjpa-persistence-jdbc/src/test/java/org/apache/openjpa/persistence/proxy/TestProxyCollection.java
@@ -0,0 +1,165 @@
+package org.apache.openjpa.persistence.proxy;
+
+import javax.persistence.EntityManager;
+
+import org.apache.openjpa.persistence.test.SingleEMFTestCase;
+import org.apache.openjpa.util.ChangeTracker;
+import org.apache.openjpa.util.ProxyCollection;
+
+/**
+ * Tests proxying and change tracking of collection fields for modification in
+ * detached state.
+ *
+ * Originally reported in
+ * OPENJPA-628
+ *
+ * @author Pinaki Poddar
+ *
+ */
+public class TestProxyCollection extends SingleEMFTestCase {
+ public void setUp() {
+ super.setUp(CLEAR_TABLES, TreeNode.class);
+ }
+ /**
+ * Tests that a uniform tree is created with expected fan outs at each
+ * level. This is not a persistent operation, just in-memory.
+ */
+ public void testCreateTree() {
+ TreeNode root = new TreeNode();
+ root.setName("0");
+ int[] fanOuts = {1,2,3};
+ root.createTree(fanOuts);
+ assertArrayEquals(fanOuts, root.getFanOuts());
+ }
+
+ /**
+ * Tests that a uniform tree can be modified with different fan outs at each
+ * level. This is not a persistent operation, just in-memory.
+ */
+ public void testModifyTree() {
+ int[] fanOuts = {1,2,2,4};
+ int[] newFanOuts = {1,3,1,2};
+ TreeNode root = new TreeNode();
+ root.createTree(fanOuts);
+ assertArrayEquals(fanOuts, root.getFanOuts());
+
+ root.modify(newFanOuts);
+ assertArrayEquals(newFanOuts, root.getFanOuts());
+ }
+
+ /**
+ * Tests that a uniform tree is persisted and later fetched back with same
+ * number of children at every level.
+ */
+ public void testPersistTree() {
+ int[] fanOuts = {2,3,4};
+ verify(create(fanOuts), fanOuts);
+ }
+
+ public void testAddNodeAtLeaf() {
+ int[] original = {1,2,3};
+ int[] modifier = {1,2,4}; // add new child at Level 2
+ createModifyAndMerge(original, modifier);
+ }
+
+ public void testAddNewLevel() {
+ int[] original = {1,2,3};
+ int[] modifier = {1,2,3,2}; // add 2 new children at new Level
+ createModifyAndMerge(original, modifier);
+ }
+
+ public void testAddAndRemove() {
+ int[] original = {2,3,4};
+ int[] modifier = {4,3,2}; // add 1 at Level 1 + remove 1 at Level 3
+ createModifyAndMerge(original, modifier);
+ }
+
+ public void testAddAtAllLevel() {
+ int[] original = {2,3,4};
+ int[] modifier = {3,4,5}; // add 1 at each Level
+ createModifyAndMerge(original, modifier);
+ }
+
+ public void testRemoveAtAllLevel() {
+ int[] original = {2,3,4};
+ int[] modifier = {1,2,3}; // remove 1 from each Level
+ createModifyAndMerge(original, modifier);
+ }
+ /**
+ * Create a uniform tree with original fanout.
+ * Persist.
+ * Verify in a separate persistence context that the tree is stored.
+ * Modify the tree by adding or deleting nodes according to the given
+ * modified fanouts outside a transaction.
+ * Merge the changes.
+ * Verify that the changes are merged by fetching the modified version.
+ *
+ * @param original
+ * @param modified
+ */
+ void createModifyAndMerge(int[] original, int[] modifier) {
+ TreeNode root = create(original);
+
+ EntityManager em = emf.createEntityManager();
+ em.getTransaction().begin();
+ TreeNode modified = em.find(TreeNode.class, root.getId());
+ modified.modify(modifier);
+ em.merge(modified);
+ em.getTransaction().commit();
+ em.clear();
+
+ assertProxyCollection(root.getNodes(), false);
+
+ verify(root, modifier);
+ }
+
+ /**
+ * Create a uniform tree with given fan out.
+ * Persist.
+ * Verify that the tree is stored by fetching it in a separate persistence
+ * context.
+ */
+ TreeNode create(int[] original) {
+ TreeNode root = new TreeNode();
+ root.createTree(original);
+
+ EntityManager em = emf.createEntityManager();
+ em.getTransaction().begin();
+ em.persist(root);
+ em.getTransaction().commit();
+ em.clear();
+
+ return root;
+ }
+
+ void verify(TreeNode node, int[] fanOuts) {
+ EntityManager em = emf.createEntityManager();
+ em.getTransaction().begin();
+ TreeNode test = em.find(TreeNode.class, node.getId());
+ assertNotNull(test);
+ assertArrayEquals(fanOuts, test.getFanOuts());
+ }
+
+ /** Asserts the given arrays have exactly same elements at the same index.
+ */
+ void assertArrayEquals(int[] a, int[] b) {
+ assertEquals(a.length, b.length);
+ for (int i = 0; i childern = new ArrayList();
+
+ @Version
+ private int version;
+
+ public long getId() {
+ return id;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ /**
+ * Add a child node at the end of the current list of children.
+ */
+ public void addNode(TreeNode node) {
+ addNode(node, childern.size());
+ }
+
+ /**
+ * Insert a child node at the specified position in the list of children.
+ */
+ public void addNode(TreeNode node, int position) {
+ checkSequenceRange(position);
+ childern.add(position, node);
+ }
+
+ public boolean removeNode(TreeNode node) {
+ return childern.remove(node);
+ }
+
+ public TreeNode removeNode(int sequence) {
+ checkSequenceRange(sequence);
+ return childern.remove(sequence);
+ }
+
+ public TreeNode getNode(int sequence) {
+ checkSequenceRange(sequence);
+ return childern.get(sequence);
+ }
+
+ public List getNodes() {
+ return childern;
+ }
+
+ public void clearNodes() {
+ childern.clear();
+ }
+
+ public boolean isLeaf() {
+ return childern.isEmpty();
+ }
+
+ protected void checkSequenceRange(int sequence)
+ throws IllegalArgumentException {
+ int size = childern.size();
+ if (sequence < 0 || sequence > size)
+ throw new IllegalArgumentException("Sequence number is beyond "
+ + "range of 0 to " + size + ".");
+ }
+
+ public int getVersion() {
+ return version;
+ }
+
+ /**
+ * Create a uniform subtree below the receiver. Uniform subtree implies that
+ * each child at a level L has equal number of grand children at level L+1.
+ *
+ * @param fanOuts
+ * array of fan outs for children at every level.
+ */
+ public void createTree(int[] fanOuts) {
+ if (fanOuts.length == 0)
+ return;
+ int[] nextFanOuts = new int[fanOuts.length];
+ System.arraycopy(fanOuts, 1, nextFanOuts, 0, fanOuts.length - 1);
+ for (int j = 0; j < fanOuts[0]; j++) {
+ TreeNode child = new TreeNode();
+ child.setName(getName() + "." + j);
+ addNode(child);
+ child.createTree(nextFanOuts);
+ }
+ }
+
+ /**
+ * Add or remove subtree of the receiver to match the given fanOut.
+ */
+ public void modify(int[] fanOuts) {
+ if (fanOuts == null || fanOuts.length == 0)
+ return;
+ int n = fanOuts[0];
+ int[] nextFanOuts = new int[fanOuts.length];
+ System.arraycopy(fanOuts, 1, nextFanOuts, 0, fanOuts.length - 1);
+ List children = getNodes();
+ int diff = children.size() - n;
+ if (diff < 0) {
+ for (int i = 0; i < -diff; i++) {
+ TreeNode newChild = new TreeNode();
+ int position = getNodes().size();
+ newChild.setName(getName() + "." + position);
+ addNode(newChild);
+ }
+ } else if (diff > 0) {
+ for (int i = 0; i < diff; i++) {
+ int position = getNodes().size() - 1;
+ removeNode(position);
+ }
+ }
+ children = getNodes();
+ for (TreeNode child : children) {
+ child.modify(nextFanOuts);
+ }
+ }
+
+ /**
+ * Get the fan outs of the given receiver. Assumes that the subtree is
+ * uniform. Otherwise throws exception.
+ */
+ public int[] getFanOuts() {
+ return getFanOuts(new int[] {});
+ }
+
+ private int[] getFanOuts(int[] list) {
+ List children = getNodes();
+ if (children.isEmpty())
+ return list;
+ int[] fanOuts = new int[children.size()];
+ int i = 0;
+ for (TreeNode child : children) {
+ fanOuts[i++] = child.getNodes().size();
+ }
+ for (int j = 0; j < fanOuts.length - 1; j++)
+ if (fanOuts[j] != fanOuts[j + 1])
+ throw new RuntimeException("non-uniform fanouts for children "
+ + " of " + getName());
+
+ int[] newList = new int[list.length + 1];
+ System.arraycopy(list, 0, newList, 0, list.length);
+ newList[list.length] = children.size();
+ return children.get(0).getFanOuts(newList);
+ }
+
+ /**
+ * Prints this receiver and its subtree.
+ */
+ public void print(PrintStream out) {
+ print(2, out);
+ }
+
+ private void print(int tab, PrintStream out) {
+ for (int i = 0; i < tab; i++)
+ out.print(" ");
+ out.println(getName());
+ for (TreeNode child : getNodes()) {
+ child.print(tab + 2, out);
+ }
+ }
+
+}