THRIFT-5700: Migrate to JakartaEE and Apache HttpComponents 5 (#2746)

- Migrate to JakartaEE and Apache HttpComponents 5
- Modify C code to choose which namespace choose for "Generated" annotations (jakarta by default)
diff --git a/compiler/cpp/src/thrift/generate/t_java_generator.cc b/compiler/cpp/src/thrift/generate/t_java_generator.cc
index 7e55bf9..172d137 100644
--- a/compiler/cpp/src/thrift/generate/t_java_generator.cc
+++ b/compiler/cpp/src/thrift/generate/t_java_generator.cc
@@ -80,6 +80,7 @@
     rethrow_unhandled_exceptions_ = false;
     unsafe_binaries_ = false;
     annotations_as_metadata_ = false;
+    jakarta_annotations_ = false;
     for (iter = parsed_options.begin(); iter != parsed_options.end(); ++iter) {
       if (iter->first.compare("beans") == 0) {
         bean_style_ = true;
@@ -128,6 +129,8 @@
         unsafe_binaries_ = true;
       } else if (iter->first.compare("annotations_as_metadata") == 0) {
         annotations_as_metadata_ = true;
+      } else if (iter->first.compare("jakarta_annotations") == 0) {
+        jakarta_annotations_ = true;
       } else {
         throw "unknown option java:" + iter->first;
       }
@@ -465,6 +468,7 @@
   bool rethrow_unhandled_exceptions_;
   bool unsafe_binaries_;
   bool annotations_as_metadata_;
+  bool jakarta_annotations_;
 };
 
 /**
@@ -5808,7 +5812,12 @@
 void t_java_generator::generate_javax_generated_annotation(ostream& out) {
   time_t seconds = time(nullptr);
   struct tm* now = localtime(&seconds);
-  indent(out) << "@javax.annotation.Generated(value = \"" << autogen_summary() << "\"";
+  if (jakarta_annotations_) {
+    indent(out) << "@jakarta.annotation.Generated(value = \"" << autogen_summary() << "\"";
+  } else {
+    indent(out) << "@javax.annotation.Generated(value = \"" << autogen_summary() << "\"";
+  }
+
   if (undated_generated_annotations_) {
     out << ")" << endl;
   } else {
@@ -5849,5 +5858,6 @@
     "                     undated: suppress the date at @Generated annotations\n"
     "                     suppress: suppress @Generated annotations entirely\n"
     "    unsafe_binaries: Do not copy ByteBuffers in constructors, getters, and setters.\n"
+    "    jakarta_annotations: generate jakarta annotations (javax by default)\n"
     "    annotations_as_metadata:\n"
     "                     Include Thrift field annotations as metadata in the generated code.\n")
diff --git a/lib/java/gradle.properties b/lib/java/gradle.properties
index bfd5c22..2e3d041 100644
--- a/lib/java/gradle.properties
+++ b/lib/java/gradle.properties
@@ -25,14 +25,14 @@
 maven-repository-id=apache.releases.https
 
 # Dependency versions
-httpclient.version=4.5.13
-httpcore.version=4.4.15
+httpclient.version=5.2.1
+httpcore.version=5.2
 slf4j.version=1.7.36
-servlet.version=4.0.3
-tomcat.embed.version=9.0.43
-junit.version=5.8.2
+servlet.version=5.0.0
+tomcat.embed.version=10.1.4
+junit.version=5.9.1
 mockito.version=5.3.0
-javax.annotation.version=1.3.5
+javax.annotation.version=2.1.1
 commons-lang3.version=3.12.0
 
 org.gradle.jvmargs=--add-exports jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED \
diff --git a/lib/java/gradle/environment.gradle b/lib/java/gradle/environment.gradle
index 8c14a52..977c07e 100644
--- a/lib/java/gradle/environment.gradle
+++ b/lib/java/gradle/environment.gradle
@@ -61,8 +61,8 @@
 
 dependencies {
     implementation "org.slf4j:slf4j-api:${slf4jVersion}"
-    implementation "org.apache.httpcomponents:httpclient:${httpclientVersion}"
-    implementation "org.apache.httpcomponents:httpcore:${httpcoreVersion}"
+    implementation "org.apache.httpcomponents.client5:httpclient5:${httpclientVersion}"
+    implementation "org.apache.httpcomponents.core5:httpcore5:${httpcoreVersion}"
     implementation "jakarta.servlet:jakarta.servlet-api:${servletVersion}"
     implementation "jakarta.annotation:jakarta.annotation-api:${javaxAnnotationVersion}"
     implementation "org.apache.commons:commons-lang3:${commonsLang3Version}"
diff --git a/lib/java/gradle/generateTestThrift.gradle b/lib/java/gradle/generateTestThrift.gradle
index 10bb16b..7e33b15 100644
--- a/lib/java/gradle/generateTestThrift.gradle
+++ b/lib/java/gradle/generateTestThrift.gradle
@@ -38,7 +38,7 @@
 // Code generation for Unit Testing
 
 // A callable closure to make this easier
-ext.thriftCompile = { Task task, String thriftFileName, String generator = 'java', File outputDir = genSrc ->
+ext.thriftCompile = { Task task, String thriftFileName, String generator = 'java:jakarta_annotations', File outputDir = genSrc ->
     def thriftFile = file("$thriftRoot/test/$thriftFileName")
     if (!thriftFile.exists()) {
         thriftFile = file("$projectDir/src/test/resources/$thriftFileName")
@@ -98,7 +98,7 @@
 
     ext.outputBuffer = new ByteArrayOutputStream()
 
-    thriftCompile(it, 'JavaOptionTypeJdk8Test.thrift', 'java:option_type=jdk8', genOptionTypeJdk8Src)
+    thriftCompile(it, 'JavaOptionTypeJdk8Test.thrift', 'java:option_type=jdk8,jakarta_annotations', genOptionTypeJdk8Src)
 }
 
 task generateBeanJava(group: 'Build') {
@@ -107,7 +107,7 @@
 
     ext.outputBuffer = new ByteArrayOutputStream()
 
-    thriftCompile(it, 'JavaBeansTest.thrift', 'java:beans,nocamel,future_iface', genBeanSrc)
+    thriftCompile(it, 'JavaBeansTest.thrift', 'java:beans,nocamel,future_iface,jakarta_annotations', genBeanSrc)
 }
 
 task generateReuseJava(group: 'Build') {
@@ -116,7 +116,7 @@
 
     ext.outputBuffer = new ByteArrayOutputStream()
 
-    thriftCompile(it, 'FullCamelTest.thrift', 'java:fullcamel,future_iface', genFullCamelSrc)
+    thriftCompile(it, 'FullCamelTest.thrift', 'java:fullcamel,future_iface,jakarta_annotations', genFullCamelSrc)
 }
 
 task generateFullCamelJava(group: 'Build') {
@@ -125,7 +125,7 @@
 
     ext.outputBuffer = new ByteArrayOutputStream()
 
-    thriftCompile(it, 'ReuseObjects.thrift', 'java:reuse_objects', genReuseSrc)
+    thriftCompile(it, 'ReuseObjects.thrift', 'java:reuse_objects,jakarta_annotations', genReuseSrc)
 }
 
 task generateUnsafeBinariesJava(group: 'Build') {
@@ -134,7 +134,7 @@
 
     ext.outputBuffer = new ByteArrayOutputStream()
 
-    thriftCompile(it, 'UnsafeTypes.thrift', 'java:unsafe_binaries', genUnsafeSrc)
+    thriftCompile(it, 'UnsafeTypes.thrift', 'java:unsafe_binaries,jakarta_annotations', genUnsafeSrc)
 }
 
 task generateWithAnnotationMetadata(group: 'Build') {
@@ -143,7 +143,7 @@
 
     ext.outputBuffer = new ByteArrayOutputStream()
 
-    thriftCompile(it, 'JavaAnnotationTest.thrift', 'java:annotations_as_metadata', genSrc)
+    thriftCompile(it, 'JavaAnnotationTest.thrift', 'java:annotations_as_metadata,jakarta_annotations', genSrc)
 }
 
 task generateJavaDefinitionOrderTestJava(group: 'Build') {
@@ -152,6 +152,6 @@
 
     ext.outputBuffer = new ByteArrayOutputStream()
 
-    thriftCompile(it, 'JavaDefinitionOrderA.thrift', 'java', genDefinitionOrderTestASrc)
-    thriftCompile(it, 'JavaDefinitionOrderB.thrift', 'java', genDefinitionOrderTestBSrc)
+    thriftCompile(it, 'JavaDefinitionOrderA.thrift', 'java:jakarta_annotations', genDefinitionOrderTestASrc)
+    thriftCompile(it, 'JavaDefinitionOrderB.thrift', 'java:jakarta_annotations', genDefinitionOrderTestBSrc)
 }
diff --git a/lib/java/src/crossTest/java/org/apache/thrift/test/TestClient.java b/lib/java/src/crossTest/java/org/apache/thrift/test/TestClient.java
index 2807a43..54f3424 100644
--- a/lib/java/src/crossTest/java/org/apache/thrift/test/TestClient.java
+++ b/lib/java/src/crossTest/java/org/apache/thrift/test/TestClient.java
@@ -29,7 +29,7 @@
 import java.util.Set;
 import java.util.UUID;
 import java.util.stream.IntStream;
-import org.apache.http.impl.client.HttpClients;
+import org.apache.hc.client5.http.impl.classic.HttpClients;
 import org.apache.thrift.TApplicationException;
 import org.apache.thrift.TException;
 import org.apache.thrift.TSerializer;
diff --git a/lib/java/src/main/java/org/apache/thrift/THttpClientResponseHandler.java b/lib/java/src/main/java/org/apache/thrift/THttpClientResponseHandler.java
new file mode 100644
index 0000000..1ed1318
--- /dev/null
+++ b/lib/java/src/main/java/org/apache/thrift/THttpClientResponseHandler.java
@@ -0,0 +1,74 @@
+package org.apache.thrift;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import org.apache.hc.core5.http.ClassicHttpResponse;
+import org.apache.hc.core5.http.HttpEntity;
+import org.apache.hc.core5.http.HttpException;
+import org.apache.hc.core5.http.HttpStatus;
+import org.apache.hc.core5.http.io.HttpClientResponseHandler;
+
+public class THttpClientResponseHandler implements HttpClientResponseHandler<InputStream> {
+  @Override
+  public InputStream handleResponse(ClassicHttpResponse response)
+      throws HttpException, IOException {
+    try (InputStream is = response.getEntity().getContent()) {
+      int responseCode = response.getCode();
+      if (responseCode != HttpStatus.SC_OK) {
+        throw new IOException("HTTP Response code: " + responseCode);
+      }
+      byte[] readByteArray = readIntoByteArray(is);
+      try {
+        // Indicate we're done with the content.
+        consume(response.getEntity());
+      } catch (IOException ioe) {
+        // We ignore this exception, it might only mean the server has no
+        // keep-alive capability.
+      }
+      return new ByteArrayInputStream(readByteArray);
+    } catch (IOException ioe) {
+      throw ioe;
+    }
+  }
+
+  /**
+   * Read the responses into a byte array so we can release the connection early. This implies that
+   * the whole content will have to be read in memory, and that momentarily we might use up twice
+   * the memory (while the thrift struct is being read up the chain). Proceeding differently might
+   * lead to exhaustion of connections and thus to app failure.
+   *
+   * @param is input stream
+   * @return read bytes
+   * @throws IOException when exception during read
+   */
+  private static byte[] readIntoByteArray(InputStream is) throws IOException {
+    ByteArrayOutputStream baos = new ByteArrayOutputStream();
+    byte[] buf = new byte[1024];
+    int len;
+    do {
+      len = is.read(buf);
+      if (len > 0) {
+        baos.write(buf, 0, len);
+      }
+    } while (-1 != len);
+    return baos.toByteArray();
+  }
+
+  /**
+   * copy from org.apache.http.util.EntityUtils#consume. Android has it's own httpcore that doesn't
+   * have a consume.
+   */
+  private static void consume(final HttpEntity entity) throws IOException {
+    if (entity == null) {
+      return;
+    }
+    if (entity.isStreaming()) {
+      InputStream instream = entity.getContent();
+      if (instream != null) {
+        instream.close();
+      }
+    }
+  }
+}
diff --git a/lib/java/src/main/java/org/apache/thrift/server/TExtensibleServlet.java b/lib/java/src/main/java/org/apache/thrift/server/TExtensibleServlet.java
index fa5acac..91f0515 100644
--- a/lib/java/src/main/java/org/apache/thrift/server/TExtensibleServlet.java
+++ b/lib/java/src/main/java/org/apache/thrift/server/TExtensibleServlet.java
@@ -19,18 +19,18 @@
 
 package org.apache.thrift.server;
 
+import jakarta.servlet.ServletConfig;
+import jakarta.servlet.ServletContext;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServlet;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.OutputStream;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Map;
-import javax.servlet.ServletConfig;
-import javax.servlet.ServletContext;
-import javax.servlet.ServletException;
-import javax.servlet.http.HttpServlet;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
 import org.apache.thrift.TException;
 import org.apache.thrift.TProcessor;
 import org.apache.thrift.protocol.TProtocol;
diff --git a/lib/java/src/main/java/org/apache/thrift/server/TServlet.java b/lib/java/src/main/java/org/apache/thrift/server/TServlet.java
index 831842b..dbdf615 100644
--- a/lib/java/src/main/java/org/apache/thrift/server/TServlet.java
+++ b/lib/java/src/main/java/org/apache/thrift/server/TServlet.java
@@ -1,15 +1,15 @@
 package org.apache.thrift.server;
 
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServlet;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.OutputStream;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Map;
-import javax.servlet.ServletException;
-import javax.servlet.http.HttpServlet;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
 import org.apache.thrift.TException;
 import org.apache.thrift.TProcessor;
 import org.apache.thrift.protocol.TProtocol;
diff --git a/lib/java/src/main/java/org/apache/thrift/transport/THttpClient.java b/lib/java/src/main/java/org/apache/thrift/transport/THttpClient.java
index e514dd5..3a5ec36 100644
--- a/lib/java/src/main/java/org/apache/thrift/transport/THttpClient.java
+++ b/lib/java/src/main/java/org/apache/thrift/transport/THttpClient.java
@@ -19,7 +19,6 @@
 
 package org.apache.thrift.transport;
 
-import java.io.ByteArrayInputStream;
 import java.io.ByteArrayOutputStream;
 import java.io.IOException;
 import java.io.InputStream;
@@ -28,15 +27,15 @@
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.Map;
-import org.apache.http.HttpEntity;
-import org.apache.http.HttpHost;
-import org.apache.http.HttpResponse;
-import org.apache.http.HttpStatus;
-import org.apache.http.client.HttpClient;
-import org.apache.http.client.config.RequestConfig;
-import org.apache.http.client.methods.HttpPost;
-import org.apache.http.entity.ByteArrayEntity;
+import org.apache.hc.client5.http.classic.HttpClient;
+import org.apache.hc.client5.http.classic.methods.HttpPost;
+import org.apache.hc.client5.http.config.ConnectionConfig;
+import org.apache.hc.client5.http.config.RequestConfig;
+import org.apache.hc.core5.http.HttpHost;
+import org.apache.hc.core5.http.io.entity.ByteArrayEntity;
+import org.apache.hc.core5.util.Timeout;
 import org.apache.thrift.TConfiguration;
+import org.apache.thrift.THttpClientResponseHandler;
 
 /**
  * HTTP implementation of the TTransport interface. Used for working with a Thrift web services
@@ -139,9 +138,9 @@
       this.client = client;
       this.host =
           new HttpHost(
+              url_.getProtocol(),
               url_.getHost(),
-              -1 == url_.getPort() ? url_.getDefaultPort() : url_.getPort(),
-              url_.getProtocol());
+              -1 == url_.getPort() ? url_.getDefaultPort() : url_.getPort());
     } catch (IOException iox) {
       throw new TTransportException(iox);
     }
@@ -154,9 +153,9 @@
       this.client = client;
       this.host =
           new HttpHost(
+              url_.getProtocol(),
               url_.getHost(),
-              -1 == url_.getPort() ? url_.getDefaultPort() : url_.getPort(),
-              url_.getProtocol());
+              -1 == url_.getPort() ? url_.getDefaultPort() : url_.getPort());
     } catch (IOException iox) {
       throw new TTransportException(iox);
     }
@@ -166,6 +165,13 @@
     connectTimeout_ = timeout;
   }
 
+  /**
+   * Use instead {@link
+   * org.apache.hc.client5.http.impl.io.BasicHttpClientConnectionManager#setConnectionConfig} or
+   * {@link
+   * org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManager#setDefaultConnectionConfig}
+   */
+  @Deprecated
   public void setReadTimeout(int timeout) {
     readTimeout_ = timeout;
   }
@@ -230,14 +236,24 @@
     RequestConfig requestConfig = RequestConfig.DEFAULT;
     if (connectTimeout_ > 0) {
       requestConfig =
-          RequestConfig.copy(requestConfig).setConnectionRequestTimeout(connectTimeout_).build();
-    }
-    if (readTimeout_ > 0) {
-      requestConfig = RequestConfig.copy(requestConfig).setSocketTimeout(readTimeout_).build();
+          RequestConfig.copy(requestConfig)
+              .setConnectionRequestTimeout(Timeout.ofMilliseconds(connectTimeout_))
+              .build();
     }
     return requestConfig;
   }
 
+  private ConnectionConfig getConnectionConfig() {
+    ConnectionConfig connectionConfig = ConnectionConfig.DEFAULT;
+    if (readTimeout_ > 0) {
+      connectionConfig =
+          ConnectionConfig.copy(connectionConfig)
+              .setSocketTimeout(Timeout.ofMilliseconds(readTimeout_))
+              .build();
+    }
+    return connectionConfig;
+  }
+
   private static Map<String, String> getDefaultHeaders() {
     Map<String, String> headers = new HashMap<>();
     headers.put("Content-Type", "application/x-thrift");
@@ -246,22 +262,6 @@
     return headers;
   }
 
-  /**
-   * copy from org.apache.http.util.EntityUtils#consume. Android has it's own httpcore that doesn't
-   * have a consume.
-   */
-  private static void consume(final HttpEntity entity) throws IOException {
-    if (entity == null) {
-      return;
-    }
-    if (entity.isStreaming()) {
-      InputStream instream = entity.getContent();
-      if (instream != null) {
-        instream.close();
-      }
-    }
-  }
-
   private void flushUsingHttpClient() throws TTransportException {
     if (null == this.client) {
       throw new TTransportException("Null HttpClient, aborting.");
@@ -279,64 +279,17 @@
       if (null != customHeaders_) {
         customHeaders_.forEach(post::addHeader);
       }
-      post.setEntity(new ByteArrayEntity(data));
-      HttpResponse response = this.client.execute(this.host, post);
-      handleResponse(response);
+      post.setEntity(new ByteArrayEntity(data, null));
+      inputStream_ = client.execute(this.host, post, new THttpClientResponseHandler());
     } catch (IOException ioe) {
       // Abort method so the connection gets released back to the connection manager
       post.abort();
       throw new TTransportException(ioe);
     } finally {
       resetConsumedMessageSize(-1);
-      post.releaseConnection();
     }
   }
 
-  private void handleResponse(HttpResponse response) throws TTransportException {
-    // Retrieve the InputStream BEFORE checking the status code so
-    // resources get freed in the with clause.
-    try (InputStream is = response.getEntity().getContent()) {
-      int responseCode = response.getStatusLine().getStatusCode();
-      if (responseCode != HttpStatus.SC_OK) {
-        throw new TTransportException("HTTP Response code: " + responseCode);
-      }
-      byte[] readByteArray = readIntoByteArray(is);
-      try {
-        // Indicate we're done with the content.
-        consume(response.getEntity());
-      } catch (IOException ioe) {
-        // We ignore this exception, it might only mean the server has no
-        // keep-alive capability.
-      }
-      inputStream_ = new ByteArrayInputStream(readByteArray);
-    } catch (IOException ioe) {
-      throw new TTransportException(ioe);
-    }
-  }
-
-  /**
-   * Read the responses into a byte array so we can release the connection early. This implies that
-   * the whole content will have to be read in memory, and that momentarily we might use up twice
-   * the memory (while the thrift struct is being read up the chain). Proceeding differently might
-   * lead to exhaustion of connections and thus to app failure.
-   *
-   * @param is input stream
-   * @return read bytes
-   * @throws IOException when exception during read
-   */
-  private static byte[] readIntoByteArray(InputStream is) throws IOException {
-    ByteArrayOutputStream baos = new ByteArrayOutputStream();
-    byte[] buf = new byte[1024];
-    int len;
-    do {
-      len = is.read(buf);
-      if (len > 0) {
-        baos.write(buf, 0, len);
-      }
-    } while (-1 != len);
-    return baos.toByteArray();
-  }
-
   public void flush() throws TTransportException {
 
     if (null != this.client) {