Merge 2.x into 3.0

diff --git a/core-client/src/main/java/org/glassfish/jersey/client/ClientProperties.java b/core-client/src/main/java/org/glassfish/jersey/client/ClientProperties.java
index 838ca4b..ae46be8 100644
--- a/core-client/src/main/java/org/glassfish/jersey/client/ClientProperties.java
+++ b/core-client/src/main/java/org/glassfish/jersey/client/ClientProperties.java
@@ -494,7 +494,7 @@
      * </p>
      * @since 2.43
      */
-    public static final String SNI_HOST_NAME = "jersey.config.client.snihostname";
+    public static final String SNI_HOST_NAME = "jersey.config.client.sniHostName";
 
     /**
      * <p>The {@link javax.net.ssl.SSLContext} {@link java.util.function.Supplier} to be used to set ssl context in the current
diff --git a/core-client/src/main/java/org/glassfish/jersey/client/innate/http/SSLParamConfigurator.java b/core-client/src/main/java/org/glassfish/jersey/client/innate/http/SSLParamConfigurator.java
index 65cb5f1..49fc03c 100644
--- a/core-client/src/main/java/org/glassfish/jersey/client/innate/http/SSLParamConfigurator.java
+++ b/core-client/src/main/java/org/glassfish/jersey/client/innate/http/SSLParamConfigurator.java
@@ -18,6 +18,7 @@
 
 import org.glassfish.jersey.client.ClientProperties;
 import org.glassfish.jersey.client.ClientRequest;
+import org.glassfish.jersey.http.HttpHeaders;
 import org.glassfish.jersey.internal.PropertiesResolver;
 
 import javax.net.ssl.SSLEngine;
@@ -29,6 +30,7 @@
 import java.net.URI;
 import java.net.UnknownHostException;
 import java.util.List;
+import java.util.Locale;
 import java.util.Map;
 import java.util.Optional;
 
@@ -44,11 +46,11 @@
      * Builder of the {@link SSLParamConfigurator} instance.
      */
     public static final class Builder {
-        private ClientRequest clientRequest;
         private URI uri;
-        private Map<String, List<Object>> httpHeaders;
-        private boolean setAlways = false;
+        private String sniHostNameHeader = null;
         private String sniHostPrecedence = null;
+        private boolean setAlways = false;
+
 
         /**
          * Sets the {@link ClientRequest} instance.
@@ -56,9 +58,19 @@
          * @return the builder instance
          */
         public Builder request(ClientRequest clientRequest) {
-            this.clientRequest = clientRequest;
-            this.httpHeaders = null;
-            this.uri = null;
+            this.sniHostNameHeader = getSniHostNameHeader(clientRequest.getHeaders());
+            this.sniHostPrecedence = resolveSniHostNameProperty(clientRequest);
+            this.uri = clientRequest.getUri();
+            return this;
+        }
+
+        /**
+         * Sets the SNIHostName from the {@link Configuration} instance.
+         * @param configuration the {@link Configuration}
+         * @return the builder instance
+         */
+        public Builder configuration(Configuration configuration) {
+            this.sniHostPrecedence = getSniHostNameProperty(configuration);
             return this;
         }
 
@@ -68,7 +80,6 @@
          * @return the builder instance
          */
         public Builder uri(URI uri) {
-            this.clientRequest = null;
             this.uri = uri;
             return this;
         }
@@ -79,8 +90,7 @@
          * @return the builder instance
          */
         public Builder headers(Map<String, List<Object>> httpHeaders) {
-            this.clientRequest = null;
-            this.httpHeaders = httpHeaders;
+            this.sniHostNameHeader = getSniHostNameHeader(httpHeaders);
             return this;
         }
 
@@ -129,7 +139,7 @@
          * @return the builder instance.
          */
         public Builder setSNIHostName(Configuration configuration) {
-            return setSNIHostName((String) configuration.getProperty(ClientProperties.SNI_HOST_NAME));
+            return setSNIHostName(getSniHostNameProperty(configuration));
         }
 
         /**
@@ -148,7 +158,7 @@
          * @return the builder instance.
          */
         public Builder setSNIHostName(PropertiesResolver resolver) {
-            return setSNIHostName(resolver.resolveProperty(ClientProperties.SNI_HOST_NAME, String.class));
+            return setSNIHostName(resolveSniHostNameProperty(resolver));
         }
 
         /**
@@ -158,14 +168,38 @@
         public SSLParamConfigurator build() {
             return new SSLParamConfigurator(this);
         }
+
+        private static String getSniHostNameHeader(Map<String, List<Object>> httpHeaders) {
+            List<Object> hostHeaders = httpHeaders.get(HttpHeaders.HOST);
+            if (hostHeaders == null || hostHeaders.get(0) == null) {
+                return null;
+            }
+
+            final String hostHeader = hostHeaders.get(0).toString();
+            return hostHeader;
+        }
+
+        private static String resolveSniHostNameProperty(PropertiesResolver resolver) {
+            String property = resolver.resolveProperty(ClientProperties.SNI_HOST_NAME, String.class);
+            if (property == null) {
+                property = resolver.resolveProperty(ClientProperties.SNI_HOST_NAME.toLowerCase(Locale.ROOT), String.class);
+            }
+            return property;
+        }
+
+        private static String getSniHostNameProperty(Configuration configuration) {
+            Object property = configuration.getProperty(ClientProperties.SNI_HOST_NAME);
+            if (property == null) {
+                property = configuration.getProperty(ClientProperties.SNI_HOST_NAME.toLowerCase(Locale.ROOT));
+            }
+            return (String) property;
+        }
     }
 
     private SSLParamConfigurator(SSLParamConfigurator.Builder builder) {
-        final Map<String, List<Object>> httpHeaders =
-                builder.clientRequest != null ? builder.clientRequest.getHeaders() : builder.httpHeaders;
-        this.uri = builder.clientRequest != null ? builder.clientRequest.getUri() : builder.uri;
+        uri = builder.uri;
         if (builder.sniHostPrecedence == null) {
-            sniConfigurator = SniConfigurator.createWhenHostHeader(uri, httpHeaders, builder.setAlways);
+            sniConfigurator = SniConfigurator.createWhenHostHeader(uri, builder.sniHostNameHeader, builder.setAlways);
         } else {
             // Do not set SNI always, the property can be used to turn the SNI off
             sniConfigurator = SniConfigurator.createWhenHostHeader(uri, builder.sniHostPrecedence, false);
diff --git a/core-client/src/main/java/org/glassfish/jersey/client/innate/http/SniConfigurator.java b/core-client/src/main/java/org/glassfish/jersey/client/innate/http/SniConfigurator.java
index 59c8422..cb1d937 100644
--- a/core-client/src/main/java/org/glassfish/jersey/client/innate/http/SniConfigurator.java
+++ b/core-client/src/main/java/org/glassfish/jersey/client/innate/http/SniConfigurator.java
@@ -51,24 +51,6 @@
     }
 
     /**
-     * Create {@link SniConfigurator} when {@link HttpHeaders#HOST} is set different from the request URI host
-     * (or {@code whenDiffer}.is false).
-     * @param hostUri the Uri of the HTTP request
-     * @param headers the HttpHeaders
-     * @param whenDiffer create {@SniConfigurator only when different from the request URI host}
-     * @return Optional {@link SniConfigurator} or empty when {@link HttpHeaders#HOST} is equal to the requestHost
-     */
-    static Optional<SniConfigurator> createWhenHostHeader(URI hostUri, Map<String, List<Object>> headers, boolean whenDiffer) {
-        List<Object> hostHeaders = headers.get(HttpHeaders.HOST);
-        if (hostHeaders == null || hostHeaders.get(0) == null) {
-            return Optional.empty();
-        }
-
-        final String hostHeader = hostHeaders.get(0).toString();
-        return createWhenHostHeader(hostUri, hostHeader, whenDiffer);
-    }
-
-    /**
      * Create {@link SniConfigurator} when {@code sniHost} is set different from the request URI host
      * (or {@code whenDiffer}.is false).
      * @param hostUri the Uri of the HTTP request
diff --git a/core-client/src/test/java/org/glassfish/jersey/client/innate/http/SSLParamConfiguratorTest.java b/core-client/src/test/java/org/glassfish/jersey/client/innate/http/SSLParamConfiguratorTest.java
new file mode 100644
index 0000000..06dcdec
--- /dev/null
+++ b/core-client/src/test/java/org/glassfish/jersey/client/innate/http/SSLParamConfiguratorTest.java
@@ -0,0 +1,158 @@
+/*
+ * Copyright (c) 2024 Oracle and/or its affiliates. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0, which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the
+ * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
+ * version 2 with the GNU Classpath Exception, which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ */
+
+package org.glassfish.jersey.client.innate.http;
+
+import org.glassfish.jersey.client.ClientConfig;
+import org.glassfish.jersey.client.ClientProperties;
+import org.glassfish.jersey.client.ClientRequest;
+import org.glassfish.jersey.client.JerseyClient;
+import org.glassfish.jersey.http.HttpHeaders;
+import org.glassfish.jersey.internal.MapPropertiesDelegate;
+import org.glassfish.jersey.internal.PropertiesDelegate;
+import org.hamcrest.MatcherAssert;
+import org.hamcrest.Matchers;
+import org.junit.jupiter.api.Test;
+
+import jakarta.ws.rs.client.ClientBuilder;
+import jakarta.ws.rs.core.MultivaluedHashMap;
+
+import java.net.URI;
+import java.util.Collections;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+
+public class SSLParamConfiguratorTest {
+    @Test
+    public void testNoHost() {
+        final URI uri = URI.create("http://xxx.com:8080");
+        final JerseyClient client = (JerseyClient) ClientBuilder.newClient();
+        final ClientConfig config = client.getConfiguration();
+        final PropertiesDelegate delegate = new MapPropertiesDelegate();
+        ClientRequest request = new ClientRequest(uri, config, delegate) {};
+        SSLParamConfigurator configurator = SSLParamConfigurator.builder().request(request).build();
+        MatcherAssert.assertThat(configurator.isSNIRequired(), Matchers.is(false));
+    }
+
+    @Test
+    public void testHostHeaderHasPrecedence() {
+        final URI uri = URI.create("http://xxx.com:8080");
+        final JerseyClient client = (JerseyClient) ClientBuilder.newClient();
+        final ClientConfig config = client.getConfiguration();
+        final PropertiesDelegate delegate = new MapPropertiesDelegate();
+        ClientRequest request = new ClientRequest(uri, config, delegate) {};
+        request.getHeaders().add(HttpHeaders.HOST, "yyy.com");
+        SSLParamConfigurator configurator = SSLParamConfigurator.builder().request(request).build();
+        MatcherAssert.assertThat(configurator.isSNIRequired(), Matchers.is(true));
+        MatcherAssert.assertThat(configurator.getSNIHostName(), Matchers.is("yyy.com"));
+    }
+
+    @Test
+    public void testPropertyOnClientHasPrecedence() {
+        final URI uri = URI.create("http://xxx.com:8080");
+        final JerseyClient client = (JerseyClient) ClientBuilder.newClient();
+        final ClientConfig config = client.getConfiguration();
+        final PropertiesDelegate delegate = new MapPropertiesDelegate();
+        client.property(ClientProperties.SNI_HOST_NAME, "yyy.com");
+        ClientRequest request = new ClientRequest(uri, config, delegate) {};
+        SSLParamConfigurator configurator = SSLParamConfigurator.builder().request(request).build();
+        MatcherAssert.assertThat(configurator.isSNIRequired(), Matchers.is(true));
+        MatcherAssert.assertThat(configurator.getSNIHostName(), Matchers.is("yyy.com"));
+    }
+
+    @Test
+    public void testPropertyOnDelegateHasPrecedence() {
+        final URI uri = URI.create("http://xxx.com:8080");
+        final JerseyClient client = (JerseyClient) ClientBuilder.newClient();
+        final ClientConfig config = client.getConfiguration();
+        final PropertiesDelegate delegate = new MapPropertiesDelegate();
+        client.property(ClientProperties.SNI_HOST_NAME, "yyy.com");
+        delegate.setProperty(ClientProperties.SNI_HOST_NAME, "zzz.com");
+        ClientRequest request = new ClientRequest(uri, config, delegate) {};
+        SSLParamConfigurator configurator = SSLParamConfigurator.builder().request(request).build();
+        MatcherAssert.assertThat(configurator.isSNIRequired(), Matchers.is(true));
+        MatcherAssert.assertThat(configurator.getSNIHostName(), Matchers.is("zzz.com"));
+    }
+
+    @Test
+    public void testPropertyOnDelegateHasPrecedenceOverHost() {
+        final URI uri = URI.create("http://xxx.com:8080");
+        final JerseyClient client = (JerseyClient) ClientBuilder.newClient();
+        final ClientConfig config = client.getConfiguration();
+        final PropertiesDelegate delegate = new MapPropertiesDelegate();
+        client.property(ClientProperties.SNI_HOST_NAME, "yyy.com");
+        delegate.setProperty(ClientProperties.SNI_HOST_NAME, "zzz.com");
+        ClientRequest request = new ClientRequest(uri, config, delegate) {};
+        request.getHeaders().add(HttpHeaders.HOST, "www.com");
+        SSLParamConfigurator configurator = SSLParamConfigurator.builder().request(request).build();
+        MatcherAssert.assertThat(configurator.isSNIRequired(), Matchers.is(true));
+        MatcherAssert.assertThat(configurator.getSNIHostName(), Matchers.is("zzz.com"));
+    }
+
+    @Test
+    public void testDisableSni() {
+        final URI uri = URI.create("http://xxx.com:8080");
+        final JerseyClient client = (JerseyClient) ClientBuilder.newClient();
+        final ClientConfig config = client.getConfiguration();
+        final PropertiesDelegate delegate = new MapPropertiesDelegate();
+        client.property(ClientProperties.SNI_HOST_NAME, "yyy.com");
+        delegate.setProperty(ClientProperties.SNI_HOST_NAME, "xxx.com");
+        ClientRequest request = new ClientRequest(uri, config, delegate) {};
+        request.getHeaders().add(HttpHeaders.HOST, "www.com");
+        SSLParamConfigurator configurator = SSLParamConfigurator.builder().request(request).build();
+        MatcherAssert.assertThat(configurator.isSNIRequired(), Matchers.is(false));
+        MatcherAssert.assertThat(configurator.getSNIHostName(), Matchers.is("xxx.com"));
+    }
+
+    @Test
+    public void testLowerCasePropertyOnClientHasPrecedence() {
+        final URI uri = URI.create("http://xxx.com:8080");
+        final JerseyClient client = (JerseyClient) ClientBuilder.newClient();
+        final ClientConfig config = client.getConfiguration();
+        final PropertiesDelegate delegate = new MapPropertiesDelegate();
+        client.property(ClientProperties.SNI_HOST_NAME.toLowerCase(Locale.ROOT), "yyy.com");
+        ClientRequest request = new ClientRequest(uri, config, delegate) {};
+        request.getHeaders().add(HttpHeaders.HOST, "www.com");
+        SSLParamConfigurator configurator = SSLParamConfigurator.builder().request(request).build();
+        MatcherAssert.assertThat(configurator.isSNIRequired(), Matchers.is(true));
+        MatcherAssert.assertThat(configurator.getSNIHostName(), Matchers.is("yyy.com"));
+    }
+
+    @Test
+    public void testUriAndHeadersAndConfig() {
+        final URI uri = URI.create("http://xxx.com:8080");
+        final JerseyClient client = (JerseyClient) ClientBuilder.newClient();
+        Map<String, List<Object>> httpHeaders = new MultivaluedHashMap<>();
+        httpHeaders.put(HttpHeaders.HOST, Collections.singletonList("www.com"));
+        SSLParamConfigurator configurator = SSLParamConfigurator.builder()
+                .uri(uri)
+                .headers(httpHeaders)
+                .configuration(client.getConfiguration())
+                .build();
+        MatcherAssert.assertThat(configurator.isSNIRequired(), Matchers.is(true));
+        MatcherAssert.assertThat(configurator.getSNIHostName(), Matchers.is("www.com"));
+
+        client.property(ClientProperties.SNI_HOST_NAME, "yyy.com");
+        configurator = SSLParamConfigurator.builder()
+                .uri(uri)
+                .headers(httpHeaders)
+                .configuration(client.getConfiguration())
+                .build();
+        MatcherAssert.assertThat(configurator.isSNIRequired(), Matchers.is(true));
+        MatcherAssert.assertThat(configurator.getSNIHostName(), Matchers.is("yyy.com"));
+    }
+}
diff --git a/docs/src/main/docbook/appendix-properties.xml b/docs/src/main/docbook/appendix-properties.xml
index 5f2d0a4..b1a04c9 100644
--- a/docs/src/main/docbook/appendix-properties.xml
+++ b/docs/src/main/docbook/appendix-properties.xml
@@ -1132,7 +1132,7 @@
                     </row>
                     <row>
                         <entry>&jersey.client.ClientProperties.SNI_HOST_NAME; (Jersey 2.43 or later)</entry>
-                        <entry><literal>jersey.config.client.snihostname</literal></entry>
+                        <entry><literal>jersey.config.client.sniHostName</literal></entry>
                         <entry>
                             <para>
                                 Sets the host name to be used for calculating the <literal>javax.net.ssl.SNIHostName</literal>