Merge pull request #5733 from eclipse-ee4j/2.45-BRANCH

Technical merge of released branch 2.45-BRANCH
diff --git a/core-common/src/main/java/org/glassfish/jersey/io/package-info.java b/core-common/src/main/java/org/glassfish/jersey/io/package-info.java
new file mode 100644
index 0000000..f913ae6
--- /dev/null
+++ b/core-common/src/main/java/org/glassfish/jersey/io/package-info.java
@@ -0,0 +1,20 @@
+/*
+ * 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
+ */
+
+/**
+ * Common Jersey core io classes.
+ */
+package org.glassfish.jersey.io;
diff --git a/core-common/src/main/java/org/glassfish/jersey/io/spi/FlushedCloseable.java b/core-common/src/main/java/org/glassfish/jersey/io/spi/FlushedCloseable.java
new file mode 100644
index 0000000..12aa714
--- /dev/null
+++ b/core-common/src/main/java/org/glassfish/jersey/io/spi/FlushedCloseable.java
@@ -0,0 +1,55 @@
+/*
+ * 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.io.spi;
+
+import java.io.Closeable;
+import java.io.Flushable;
+import java.io.IOException;
+import java.io.OutputStream;
+
+/**
+ * A marker interface that the stream provided to Jersey can implement,
+ * noting that the stream does not need to call {@link #flush()} prior to {@link #close()}.
+ * That way, {@link #flush()} method is not called twice.
+ *
+ * <p>
+ *     Usable by {@link javax.ws.rs.client.ClientRequestContext#setEntityStream(OutputStream)}.
+ *     Usable by {@link javax.ws.rs.container.ContainerResponseContext#setEntityStream(OutputStream)}.
+ * </p>
+ *
+ * <p>
+ *     This marker interface can be useful for the customer OutputStream to know the {@code flush} did not come from
+ *     Jersey before close. By default, when the entity stream is to be closed by Jersey, {@code flush} is called first.
+ * </p>
+ */
+public interface FlushedCloseable extends Flushable, Closeable {
+    /**
+     * Flushes this stream by writing any buffered output to the underlying stream.
+     * Then closes this stream and releases any system resources associated
+     * with it. If the stream is already closed then invoking this
+     * method has no effect.
+     *
+     * <p> As noted in {@link AutoCloseable#close()}, cases where the
+     * close may fail require careful attention. It is strongly advised
+     * to relinquish the underlying resources and to internally
+     * <em>mark</em> the {@code Closeable} as closed, prior to throwing
+     * the {@code IOException}.
+     *
+     * @throws IOException if an I/O error occurs
+     */
+    public void close() throws IOException;
+}
diff --git a/core-common/src/main/java/org/glassfish/jersey/io/spi/package-info.java b/core-common/src/main/java/org/glassfish/jersey/io/spi/package-info.java
new file mode 100644
index 0000000..7a70945
--- /dev/null
+++ b/core-common/src/main/java/org/glassfish/jersey/io/spi/package-info.java
@@ -0,0 +1,20 @@
+/*
+ * 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
+ */
+
+/**
+ * Common Jersey core io SPI classes.
+ */
+package org.glassfish.jersey.io.spi;
diff --git a/core-common/src/main/java/org/glassfish/jersey/message/internal/OutboundMessageContext.java b/core-common/src/main/java/org/glassfish/jersey/message/internal/OutboundMessageContext.java
index dd5816f..3265a96 100644
--- a/core-common/src/main/java/org/glassfish/jersey/message/internal/OutboundMessageContext.java
+++ b/core-common/src/main/java/org/glassfish/jersey/message/internal/OutboundMessageContext.java
@@ -20,43 +20,33 @@
 import java.io.OutputStream;
 import java.lang.annotation.Annotation;
 import java.lang.reflect.Type;
-import java.net.URI;
-import java.text.ParseException;
 import java.util.ArrayList;
 import java.util.Collections;
-import java.util.Date;
-import java.util.HashMap;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Locale;
-import java.util.Map;
 import java.util.Set;
 import java.util.function.Function;
 import java.util.logging.Level;
 import java.util.logging.Logger;
 import java.util.stream.Collectors;
 
-import javax.ws.rs.ProcessingException;
 import javax.ws.rs.core.Configuration;
-import javax.ws.rs.core.Cookie;
-import javax.ws.rs.core.EntityTag;
 import javax.ws.rs.core.GenericEntity;
 import javax.ws.rs.core.GenericType;
 import javax.ws.rs.core.HttpHeaders;
 import javax.ws.rs.core.Link;
 import javax.ws.rs.core.MediaType;
 import javax.ws.rs.core.MultivaluedMap;
-import javax.ws.rs.core.NewCookie;
-import javax.ws.rs.ext.RuntimeDelegate;
 
 import org.glassfish.jersey.CommonProperties;
 import org.glassfish.jersey.internal.RuntimeDelegateDecorator;
-import org.glassfish.jersey.internal.LocalizationMessages;
 import org.glassfish.jersey.internal.util.ReflectionHelper;
 import org.glassfish.jersey.internal.util.collection.GuardianStringKeyMultivaluedMap;
 import org.glassfish.jersey.internal.util.collection.LazyValue;
 import org.glassfish.jersey.internal.util.collection.Value;
 import org.glassfish.jersey.internal.util.collection.Values;
+import org.glassfish.jersey.io.spi.FlushedCloseable;
 
 /**
  * Base outbound message context implementation.
@@ -572,11 +562,13 @@
         if (hasEntity()) {
             try {
                 final OutputStream es = getEntityStream();
-                es.flush();
+                if (!FlushedCloseable.class.isInstance(es)) {
+                    es.flush();
+                }
                 es.close();
             } catch (IOException e) {
                 // Happens when the client closed connection before receiving the full response.
-                // This is OK and not interesting in vast majority of the cases
+                // This is OK and not interesting in the vast majority of the cases
                 // hence the log level set to FINE to make sure it does not flood the log unnecessarily
                 // (especially for clients disconnecting from SSE listening, which is very common).
                 Logger.getLogger(OutboundMessageContext.class.getName()).log(Level.FINE, e.getMessage(), e);
diff --git a/tests/e2e-core-common/src/test/java/org/glassfish/jersey/tests/e2e/common/message/internal/OutboundMessageContextTest.java b/tests/e2e-core-common/src/test/java/org/glassfish/jersey/tests/e2e/common/message/internal/OutboundMessageContextTest.java
index 1327edd..a909dfa 100644
--- a/tests/e2e-core-common/src/test/java/org/glassfish/jersey/tests/e2e/common/message/internal/OutboundMessageContextTest.java
+++ b/tests/e2e-core-common/src/test/java/org/glassfish/jersey/tests/e2e/common/message/internal/OutboundMessageContextTest.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2012, 2023 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2012, 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
@@ -16,6 +16,9 @@
 
 package org.glassfish.jersey.tests.e2e.common.message.internal;
 
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
 import java.net.URI;
 import java.net.URISyntaxException;
 import java.text.ParseException;
@@ -33,10 +36,13 @@
 import javax.ws.rs.core.MediaType;
 import javax.ws.rs.ext.RuntimeDelegate;
 
+import org.glassfish.jersey.io.spi.FlushedCloseable;
 import org.glassfish.jersey.message.internal.CookieProvider;
 import org.glassfish.jersey.message.internal.OutboundMessageContext;
 import org.glassfish.jersey.tests.e2e.common.TestRuntimeDelegate;
 
+import org.hamcrest.MatcherAssert;
+import org.hamcrest.Matchers;
 import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.Test;
 import static org.hamcrest.Matchers.contains;
@@ -271,4 +277,38 @@
         newCtx.setMediaType(MediaType.APPLICATION_XML_TYPE); // new value
         Assertions.assertEquals(MediaType.APPLICATION_XML_TYPE, newCtx.getMediaType());
     }
+
+    @Test
+    public void OutboundMessageContextFlushTest() throws IOException {
+        FlushCountOutputStream os = new FlushCountOutputStream();
+        OutboundMessageContext ctx = new OutboundMessageContext((Configuration) null);
+        ctx.setEntity("Anything");
+        ctx.setEntityStream(os);
+        os.flush();
+        ctx.close();
+        MatcherAssert.assertThat(os.flushedCnt, Matchers.is(2));
+
+        os = new FlushedClosableOutputStream();
+        ctx = new OutboundMessageContext((Configuration) null);
+        ctx.setEntity("Anything2");
+        ctx.setEntityStream(os);
+        os.flush();
+        ctx.close();
+        MatcherAssert.assertThat(os.flushedCnt, Matchers.is(1));
+    }
+
+    private static class FlushCountOutputStream extends ByteArrayOutputStream {
+        private int flushedCnt = 0;
+
+        @Override
+        public void flush() throws IOException {
+            flushedCnt++;
+            super.flush();
+        }
+    }
+
+    private static class FlushedClosableOutputStream extends FlushCountOutputStream implements FlushedCloseable {
+
+    }
 }
+