From 9d39936e16655ae6e6e0348b31e7998a0e0574d8 Mon Sep 17 00:00:00 2001
From: Simone Bordet <simone.bordet@gmail.com>
Date: Mon, 9 Dec 2024 13:18:28 +0100
Subject: [PATCH] Fixes HttpFieldsMap so that now
 HttpFieldsMap.Mutable.get(name).add(value) is supported and correctly backed
 by the underlying HttpFields instance.

Also made List returned by HttpFieldsMap.Mutable.remove() and HttpFieldsMap.Immutable.get() unmodifiable.

Added test cases.

Signed-off-by: Simone Bordet <simone.bordet@gmail.com>
---
 .../org/eclipse/jetty/http/HttpFieldsMap.java |  36 +++++-
 .../eclipse/jetty/http/HttpFieldsMapTest.java | 113 ++++++++++++++++++
 2 files changed, 145 insertions(+), 4 deletions(-)
 create mode 100644 jetty-core/jetty-http/src/test/java/org/eclipse/jetty/http/HttpFieldsMapTest.java

diff --git a/jetty-core/jetty-http/src/main/java/org/eclipse/jetty/http/HttpFieldsMap.java b/jetty-core/jetty-http/src/main/java/org/eclipse/jetty/http/HttpFieldsMap.java
index f5d19855d9fa..1f66b95a0e72 100644
--- a/jetty-core/jetty-http/src/main/java/org/eclipse/jetty/http/HttpFieldsMap.java
+++ b/jetty-core/jetty-http/src/main/java/org/eclipse/jetty/http/HttpFieldsMap.java
@@ -13,8 +13,10 @@
 
 package org.eclipse.jetty.http;
 
+import java.util.AbstractList;
 import java.util.AbstractMap;
 import java.util.AbstractSet;
+import java.util.Collections;
 import java.util.Iterator;
 import java.util.List;
 import java.util.Objects;
@@ -24,6 +26,7 @@
 
 /**
  * <p>A {@link java.util.Map} which is backed by an instance of {@link HttpFields.Mutable}.</p>
+ *
  * @see HttpFieldsMap.Mutable
  * @see HttpFieldsMap.Immutable
  */
@@ -46,7 +49,32 @@ public Mutable(HttpFields.Mutable httpFields)
         public List<String> get(Object key)
         {
             if (key instanceof String s)
-                return httpFields.getValuesList(s);
+            {
+                return new AbstractList<>()
+                {
+                    private final List<String> values = httpFields.getValuesList(s);
+
+                    @Override
+                    public String get(int index)
+                    {
+                        return values.get(index);
+                    }
+
+                    @Override
+                    public int size()
+                    {
+                        return values.size();
+                    }
+
+                    @Override
+                    public boolean add(String value)
+                    {
+                        values.add(value);
+                        httpFields.add(s, value);
+                        return true;
+                    }
+                };
+            }
             return null;
         }
 
@@ -65,7 +93,7 @@ public List<String> remove(Object key)
             {
                 List<String> oldValue = get(s);
                 httpFields.remove(s);
-                return oldValue;
+                return Collections.unmodifiableList(oldValue);
             }
             return null;
         }
@@ -118,7 +146,7 @@ public int size()
     }
 
     /**
-     * <p>A {@link java.util.Map} which is backed by an instance of {@link HttpFields.Mutable}.</p>
+     * <p>A {@link java.util.Map} which is backed by an instance of {@link HttpFields}.</p>
      * <p>Any attempt to modify the map will throw {@link UnsupportedOperationException}.</p>
      */
     public static class Immutable extends HttpFieldsMap
@@ -134,7 +162,7 @@ public Immutable(HttpFields httpFields)
         public List<String> get(Object key)
         {
             if (key instanceof String s)
-                return httpFields.getValuesList(s);
+                return Collections.unmodifiableList(httpFields.getValuesList(s));
             return null;
         }
 
diff --git a/jetty-core/jetty-http/src/test/java/org/eclipse/jetty/http/HttpFieldsMapTest.java b/jetty-core/jetty-http/src/test/java/org/eclipse/jetty/http/HttpFieldsMapTest.java
new file mode 100644
index 000000000000..09c5e7e0cd90
--- /dev/null
+++ b/jetty-core/jetty-http/src/test/java/org/eclipse/jetty/http/HttpFieldsMapTest.java
@@ -0,0 +1,113 @@
+//
+// ========================================================================
+// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
+//
+// This program and the accompanying materials are made available under the
+// terms of the Eclipse Public License v. 2.0 which is available at
+// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
+// which is available at https://www.apache.org/licenses/LICENSE-2.0.
+//
+// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
+// ========================================================================
+//
+
+package org.eclipse.jetty.http;
+
+import java.util.List;
+import java.util.Map;
+
+import org.junit.jupiter.api.Test;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.equalTo;
+import static org.hamcrest.Matchers.hasEntry;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+public class HttpFieldsMapTest
+{
+    @Test
+    public void testPutField()
+    {
+        HttpFields.Mutable fields = HttpFields.build().put("foo", "bar");
+        Map<String, List<String>> map = HttpFields.asMap(fields);
+
+        map.put("baz", List.of("qux"));
+
+        assertEquals(2, map.size());
+        assertEquals(2, fields.size());
+        assertThat(map.get("foo"), equalTo(List.of("bar")));
+        assertThat(map.get("baz"), equalTo(List.of("qux")));
+        assertThat(map, hasEntry("baz", List.of("qux")));
+        assertTrue(fields.contains("foo", "bar"));
+        assertTrue(fields.contains("baz", "qux"));
+    }
+
+    @Test
+    public void testPutReplaceField()
+    {
+        HttpFields.Mutable fields = HttpFields.build().put("foo", "bar");
+        Map<String, List<String>> map = HttpFields.asMap(fields);
+
+        List<String> put = map.put("foo", List.of("baz"));
+
+        assertNotNull(put);
+        assertEquals(1, map.size());
+        assertEquals(1, fields.size());
+        assertThat(put, equalTo(List.of("bar")));
+        assertThat(map.get("foo"), equalTo(List.of("baz")));
+        assertFalse(fields.contains("foo", "bar"));
+        assertTrue(fields.contains("foo", "baz"));
+    }
+
+    @Test
+    public void testRemoveField()
+    {
+        HttpFields.Mutable fields = HttpFields.build().put("foo", "bar");
+        Map<String, List<String>> map = HttpFields.asMap(fields);
+
+        List<String> values = map.remove("foo");
+
+        assertThat(values, equalTo(List.of("bar")));
+        assertTrue(map.isEmpty());
+        assertEquals(0, fields.size());
+
+        // Adding to the values is unsupported.
+        assertThrows(UnsupportedOperationException.class, () -> values.add("baz"));
+    }
+
+    @Test
+    public void testAddValue()
+    {
+        HttpFields.Mutable fields = HttpFields.build().put("foo", List.of("bar"));
+        Map<String, List<String>> map = HttpFields.asMap(fields);
+
+        List<String> values = map.get("foo");
+        boolean added = values.add("baz");
+
+        assertTrue(added);
+        assertThat(values, equalTo(List.of("bar", "baz")));
+        assertThat(map.get("foo"), equalTo(values));
+        assertTrue(fields.contains("foo", "bar"));
+        assertTrue(fields.contains("foo", "baz"));
+        assertThat(fields.getValuesList("foo"), equalTo(values));
+    }
+
+    @Test
+    public void testRemoveValue()
+    {
+        HttpFields.Mutable fields = HttpFields.build()
+            .add("foo", "bar")
+            .add("foo", "baz");
+        Map<String, List<String>> map = HttpFields.asMap(fields);
+
+        List<String> values = map.get("foo");
+        assertEquals(2, values.size());
+
+        // Removing from the values is unsupported.
+        assertThrows(UnsupportedOperationException.class, () -> values.remove("bar"));
+    }
+}