[ 
https://issues.apache.org/jira/browse/COLLECTIONS-892?page=com.atlassian.jira.plugin.system.issuetabpanels:all-tabpanel
 ]

Ruiqi Dong updated COLLECTIONS-892:
-----------------------------------
    Description: 
*Summary*
For `uniqueIndexedCollection(...)`, adding an object whose transformed key 
already exists is supposed to fail with `IllegalArgumentException`.

The current implementation appends the object to the decorated collection 
first, and only afterwards checks the uniqueness constraint while updating the 
index. If the key collides, the exception is thrown too late: the decorated 
collection has already been mutated.
 
*Affected code*
File: 
`src/main/java/org/apache/commons/collections4/collection/IndexedCollection.java`
{code:java}
@Override
public boolean add(final C object) {
    final boolean added = super.add(object);
    if (added) {
        addToIndex(object);
    }
    return added;
}

private void addToIndex(final C object) {
    final K key = keyTransformer.apply(object);
    if (uniqueIndex && index.containsKey(key)) {
        throw new IllegalArgumentException("Duplicate key in uniquely indexed 
collection.");
    }
    index.put(key, object);
} {code}
*Reproducer*
Add the following test to 
`src/test/java/org/apache/commons/collections4/collection/IndexedCollectionTest.java`:
{code:java}
@Test
void testFailedUniqueAddDoesNotPolluteDecoratedCollection() {
    final IndexedCollection<Integer, String> indexed =
            decorateUniqueCollection(new ArrayList<>());
    indexed.add("01");

    assertThrows(IllegalArgumentException.class, () -> indexed.add("1"));
    assertEquals(1, indexed.size());
    assertEquals(asList("01"), new ArrayList<>(indexed));
} {code}
Both `"01"` and `"1"` map to the same integer key `1`.
Run:
{code:java}
mvn -q 
-Dtest=org.apache.commons.collections4.collection.IndexedCollectionTest#testFailedUniqueAddDoesNotPolluteDecoratedCollection
 test {code}
Observed behavior
{code:java}
expected: <1> but was: <2> {code}
*Expected behavior*
After the duplicate-key `add(...)` fails, the collection should still contain 
only the original element `"01"`.
This is not merely about exception atomicity in the abstract. 
`IndexedCollection` promises a unique index, but after a failed add:

- the decorated collection contains both values
- the index contains only the original key mapping

So the collection and its index immediately diverge, and the uniqueness 
guarantee is no longer reflected by the actual collection state.

  was:
# [BUG] `IndexedCollection.add()` mutates the decorated collection before 
rejecting a duplicate unique index key

## Summary

For `uniqueIndexedCollection(...)`, adding an object whose transformed key 
already exists is supposed to fail with `IllegalArgumentException`.

The current implementation appends the object to the decorated collection 
first, and only afterwards checks the uniqueness constraint while updating the 
index. If the key collides, the exception is thrown too late: the decorated 
collection has already been mutated.

## Affected code

File: 
`src/main/java/org/apache/commons/collections4/collection/IndexedCollection.java`

```java
@Override
public boolean add(final C object) {
finalbooleanadded = super.add(object);
if (added) {
addToIndex(object);
}
return added;
}

private void addToIndex(final C object) {
finalKkey = keyTransformer.apply(object);
if (uniqueIndex && index.containsKey(key)) {
thrownewIllegalArgumentException("Duplicate key in uniquely indexed 
collection.");
}
index.put(key, object);
}
```

## Reproducer

Add the following test to 
`src/test/java/org/apache/commons/collections4/collection/IndexedCollectionTest.java`:

```java
@Test
void testFailedUniqueAddDoesNotPolluteDecoratedCollection() {
finalIndexedCollection<Integer, String> indexed =
decorateUniqueCollection(newArrayList<>());
indexed.add("01");

assertThrows(IllegalArgumentException.class, () ->indexed.add("1"));
assertEquals(1, indexed.size());
assertEquals(asList("01"), newArrayList<>(indexed));
}
```

Both `"01"` and `"1"` map to the same integer key `1`.

Run:

```bash
mvn -q 
-Dtest=org.apache.commons.collections4.collection.IndexedCollectionTest#testFailedUniqueAddDoesNotPolluteDecoratedCollection
 test
```

## Observed behavior

```text
expected: <1> but was: <2>
```

## Expected behavior

After the duplicate-key `add(...)` fails, the collection should still contain 
only the original element `"01"`.

## Why this is a bug

This is not merely about exception atomicity in the abstract. 
`IndexedCollection` promises a unique index, but after a failed add:

- the decorated collection contains both values
- the index contains only the original key mapping

So the collection and its index immediately diverge, and the uniqueness 
guarantee is no longer reflected by the actual collection state.


> IndexedCollection.add() mutates the decorated collection before rejecting a 
> duplicate unique index key
> ------------------------------------------------------------------------------------------------------
>
>                 Key: COLLECTIONS-892
>                 URL: https://issues.apache.org/jira/browse/COLLECTIONS-892
>             Project: Commons Collections
>          Issue Type: Bug
>          Components: Collection
>            Reporter: Ruiqi Dong
>            Priority: Minor
>
> *Summary*
> For `uniqueIndexedCollection(...)`, adding an object whose transformed key 
> already exists is supposed to fail with `IllegalArgumentException`.
> The current implementation appends the object to the decorated collection 
> first, and only afterwards checks the uniqueness constraint while updating 
> the index. If the key collides, the exception is thrown too late: the 
> decorated collection has already been mutated.
>  
> *Affected code*
> File: 
> `src/main/java/org/apache/commons/collections4/collection/IndexedCollection.java`
> {code:java}
> @Override
> public boolean add(final C object) {
>     final boolean added = super.add(object);
>     if (added) {
>         addToIndex(object);
>     }
>     return added;
> }
> private void addToIndex(final C object) {
>     final K key = keyTransformer.apply(object);
>     if (uniqueIndex && index.containsKey(key)) {
>         throw new IllegalArgumentException("Duplicate key in uniquely indexed 
> collection.");
>     }
>     index.put(key, object);
> } {code}
> *Reproducer*
> Add the following test to 
> `src/test/java/org/apache/commons/collections4/collection/IndexedCollectionTest.java`:
> {code:java}
> @Test
> void testFailedUniqueAddDoesNotPolluteDecoratedCollection() {
>     final IndexedCollection<Integer, String> indexed =
>             decorateUniqueCollection(new ArrayList<>());
>     indexed.add("01");
>     assertThrows(IllegalArgumentException.class, () -> indexed.add("1"));
>     assertEquals(1, indexed.size());
>     assertEquals(asList("01"), new ArrayList<>(indexed));
> } {code}
> Both `"01"` and `"1"` map to the same integer key `1`.
> Run:
> {code:java}
> mvn -q 
> -Dtest=org.apache.commons.collections4.collection.IndexedCollectionTest#testFailedUniqueAddDoesNotPolluteDecoratedCollection
>  test {code}
> Observed behavior
> {code:java}
> expected: <1> but was: <2> {code}
> *Expected behavior*
> After the duplicate-key `add(...)` fails, the collection should still contain 
> only the original element `"01"`.
> This is not merely about exception atomicity in the abstract. 
> `IndexedCollection` promises a unique index, but after a failed add:
> - the decorated collection contains both values
> - the index contains only the original key mapping
> So the collection and its index immediately diverge, and the uniqueness 
> guarantee is no longer reflected by the actual collection state.



--
This message was sent by Atlassian Jira
(v8.20.10#820010)

Reply via email to