diff --git a/src/main/java/org/openrewrite/apache/httpclient5/MigrateRequestConfig.java b/src/main/java/org/openrewrite/apache/httpclient5/MigrateRequestConfig.java
new file mode 100644
index 0000000..a495237
--- /dev/null
+++ b/src/main/java/org/openrewrite/apache/httpclient5/MigrateRequestConfig.java
@@ -0,0 +1,144 @@
+/*
+ * Copyright 2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.openrewrite.apache.httpclient5;
+
+import org.jspecify.annotations.Nullable;
+import org.openrewrite.*;
+import org.openrewrite.java.*;
+import org.openrewrite.java.search.UsesMethod;
+import org.openrewrite.java.trait.Traits;
+import org.openrewrite.java.tree.J;
+import org.openrewrite.java.tree.TypeUtils;
+import org.openrewrite.marker.SearchResult;
+
+import java.util.Collections;
+import java.util.concurrent.atomic.AtomicReference;
+
+public class MigrateRequestConfig extends Recipe {
+
+ private static final String FQN_REQUEST_CONFIG = "org.apache.http.client.config.RequestConfig";
+ private static final String FQN_REQUEST_CONFIG_BUILDER = FQN_REQUEST_CONFIG + ".Builder";
+ private static final String FQN_POOL_CONN_MANAGER4 = "org.apache.http.impl.conn.PoolingHttpClientConnectionManager";
+ private static final String FQN_POOL_CONN_MANAGER5 = "org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManager";
+ private static final String FQN_HTTP_CLIENT_BUILDER = "org.apache.http.impl.client.HttpClientBuilder";
+ private static final String FQN_TIME_VALUE = "org.apache.hc.core5.util.TimeValue";
+
+ private static final String PATTERN_STALE_CHECK_ENABLED = FQN_REQUEST_CONFIG_BUILDER + " setStaleConnectionCheckEnabled(..)";
+ private static final String PATTERN_REQUEST_CONFIG = FQN_HTTP_CLIENT_BUILDER + " setDefaultRequestConfig(..)";
+ private static final MethodMatcher MATCHER_STALE_CHECK_ENABLED = new MethodMatcher(PATTERN_STALE_CHECK_ENABLED, false);
+ private static final MethodMatcher MATCHER_REQUEST_CONFIG = new MethodMatcher(PATTERN_REQUEST_CONFIG, false);
+
+ private static final String KEY_POOL_CONN_MANAGER = "poolConnManager";
+
+ @Override
+ public String getDisplayName() {
+ return "Migrate `RequestConfig` to httpclient5";
+ }
+
+ @Override
+ public String getDescription() {
+ return "Migrate `RequestConfig` to httpclient5.";
+ }
+
+ private static TreeVisitor extends Tree, ExecutionContext> callsSetStaleCheckEnabledFalse() {
+ return Traits.methodAccess(MATCHER_STALE_CHECK_ENABLED)
+ .asVisitor(access ->
+ J.Literal.isLiteralValue(access.getTree().getArguments().get(0), false) ?
+ SearchResult.found(access.getTree()) : access.getTree());
+ }
+
+ @Override
+ public TreeVisitor, ExecutionContext> getVisitor() {
+ return Preconditions.check(
+ Preconditions.and(
+ // Cheap check first, to avoid more expensive search
+ new UsesMethod<>(MATCHER_STALE_CHECK_ENABLED),
+ callsSetStaleCheckEnabledFalse()
+ ), new MigrateRequestConfigVisitor());
+ }
+
+ private static class MigrateRequestConfigVisitor extends JavaIsoVisitor {
+
+ @Override
+ public J.MethodDeclaration visitMethodDeclaration(J.MethodDeclaration method, ExecutionContext ctx) {
+ // setStaleConnectionCheckEnabled is only related to PoolingHttpClientConnectionManager
+ boolean staleEnabled = callsSetStaleCheckEnabledFalse().visitNonNull(method, ctx, getCursor().getParentOrThrow()) != method;
+ if (staleEnabled) {
+ // Find or create a new PoolingHttpClientConnectionManager
+ J.VariableDeclarations connectionManagerVD = findExistingConnectionPool(method);
+ boolean needsNewConnectionManager = connectionManagerVD == null;
+ if (needsNewConnectionManager) {
+ maybeAddImport(FQN_POOL_CONN_MANAGER5);
+ method = JavaTemplate.builder(
+ "PoolingHttpClientConnectionManager poolingHttpClientConnectionManager = " +
+ "new PoolingHttpClientConnectionManager();")
+ .javaParser(JavaParser.fromJavaVersion().classpath("httpclient5", "httpcore5"))
+ .imports(FQN_POOL_CONN_MANAGER5)
+ .build()
+ .apply(getCursor(), method.getBody().getCoordinates().firstStatement());
+ connectionManagerVD = (J.VariableDeclarations) method.getBody().getStatements().get(0);
+ }
+
+ // Set `setValidateAfterInactivity(TimeValue.NEG_ONE_MILLISECOND)`
+ J.Identifier connectionManagerIdentifier = connectionManagerVD.getVariables().get(0).getName();
+ maybeAddImport(FQN_TIME_VALUE);
+ method = JavaTemplate.builder("#{any(" + FQN_POOL_CONN_MANAGER5 + ")}.setValidateAfterInactivity(TimeValue.NEG_ONE_MILLISECOND);")
+ .javaParser(JavaParser.fromJavaVersion().classpath("httpclient5", "httpcore5"))
+ .imports(FQN_TIME_VALUE)
+ .build()
+ .apply(updateCursor(method), connectionManagerVD.getCoordinates().after(), connectionManagerIdentifier);
+
+ // Make the connection manager available to set in the method invocation visit below
+ if (needsNewConnectionManager) {
+ getCursor().putMessage(KEY_POOL_CONN_MANAGER, connectionManagerIdentifier);
+ }
+ }
+ return super.visitMethodDeclaration(method, ctx);
+ }
+
+ private J.@Nullable VariableDeclarations findExistingConnectionPool(J.MethodDeclaration method) {
+ AtomicReference existingConnManager = new AtomicReference<>();
+ new JavaIsoVisitor>() {
+ @Override
+ public J.VariableDeclarations visitVariableDeclarations(J.VariableDeclarations multiVariable, AtomicReference ref) {
+ J.VariableDeclarations vd = super.visitVariableDeclarations(multiVariable, ref);
+ if (TypeUtils.isOfClassType(vd.getTypeAsFullyQualified(), FQN_POOL_CONN_MANAGER4)) {
+ ref.set(vd);
+ }
+ return vd;
+ }
+ }.visitNonNull(method, existingConnManager, getCursor().getParentOrThrow());
+ return existingConnManager.get();
+ }
+
+ @Override
+ public J.MethodInvocation visitMethodInvocation(J.MethodInvocation method, ExecutionContext ctx) {
+ if (MATCHER_STALE_CHECK_ENABLED.matches(method)) {
+ doAfterVisit(new RemoveMethodInvocationsVisitor(Collections.singletonList(PATTERN_STALE_CHECK_ENABLED)));
+ } else if (MATCHER_REQUEST_CONFIG.matches(method)) {
+ J.Identifier connectionManagerIdentifier = getCursor().pollNearestMessage(KEY_POOL_CONN_MANAGER);
+ if (connectionManagerIdentifier != null) {
+ method = JavaTemplate.builder("#{any()}.setConnectionManager(#{any()});")
+ .javaParser(JavaParser.fromJavaVersion().classpath("httpclient5", "httpcore5"))
+ .imports(FQN_POOL_CONN_MANAGER5)
+ .build()
+ .apply(getCursor(), method.getCoordinates().replace(), method, connectionManagerIdentifier);
+ }
+ }
+ return super.visitMethodInvocation(method, ctx);
+ }
+ }
+}
diff --git a/src/main/resources/META-INF/rewrite/apache-httpclient-5.yml b/src/main/resources/META-INF/rewrite/apache-httpclient-5.yml
index d5ed625..a5a0093 100644
--- a/src/main/resources/META-INF/rewrite/apache-httpclient-5.yml
+++ b/src/main/resources/META-INF/rewrite/apache-httpclient-5.yml
@@ -41,6 +41,7 @@ recipeList:
newGroupId: org.apache.httpcomponents.core5
newArtifactId: httpcore5
newVersion: 5.3.x
+ - org.openrewrite.apache.httpclient5.MigrateRequestConfig
- org.openrewrite.apache.httpclient5.UsernamePasswordCredentials
- org.openrewrite.apache.httpclient5.UpgradeApacheHttpClient_5_ClassMapping
- org.openrewrite.apache.httpclient5.UpgradeApacheHttpClient_5_DeprecatedMethods
diff --git a/src/test/java/org/openrewrite/apache/httpclient5/UpgradeApacheHttpClient5Test.java b/src/test/java/org/openrewrite/apache/httpclient5/UpgradeApacheHttpClient5Test.java
index 1ec83fe..e51b63f 100644
--- a/src/test/java/org/openrewrite/apache/httpclient5/UpgradeApacheHttpClient5Test.java
+++ b/src/test/java/org/openrewrite/apache/httpclient5/UpgradeApacheHttpClient5Test.java
@@ -21,6 +21,7 @@
import org.openrewrite.java.JavaParser;
import org.openrewrite.test.RecipeSpec;
import org.openrewrite.test.RewriteTest;
+import org.openrewrite.test.TypeValidation;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@@ -296,4 +297,142 @@ void method() {
)
);
}
+
+ // For `setStaleConnectionCheckEnabled(true)`, keep unchanged, need another PR to migrate it
+ // Packages are changed because of other recipes
+ @Test
+ void setStaleConnectionCheckEnabledTrue() {
+ rewriteRun(
+ //language=java
+ java(
+ """
+ import org.apache.http.client.config.RequestConfig;
+ import org.apache.http.impl.client.CloseableHttpClient;
+ import org.apache.http.impl.client.HttpClientBuilder;
+
+ class Example {
+ CloseableHttpClient client() {
+ RequestConfig requestConfig = RequestConfig.custom().setStaleConnectionCheckEnabled(true).build();
+
+ return HttpClientBuilder.create()
+ .setDefaultRequestConfig(requestConfig)
+ .build();
+ }
+ }
+ """,
+ """
+ import org.apache.hc.client5.http.config.RequestConfig;
+ import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
+ import org.apache.hc.client5.http.impl.classic.HttpClientBuilder;
+
+ class Example {
+ CloseableHttpClient client() {
+ RequestConfig requestConfig = RequestConfig.custom().setStaleConnectionCheckEnabled(true).build();
+
+ return HttpClientBuilder.create()
+ .setDefaultRequestConfig(requestConfig)
+ .build();
+ }
+ }
+ """
+ )
+ );
+ }
+
+ // For `setStaleConnectionCheckEnabled(false)`, with an existing `connManager`, just call `connManager.setValidateAfterInactivity(TimeValue.NEG_ONE_MILLISECOND);`
+ @Test
+ void setStaleConnectionCheckEnabledFalseWithConnManager() {
+ rewriteRun(
+ //language=java
+ java(
+ """
+ import org.apache.http.client.config.RequestConfig;
+ import org.apache.http.impl.client.CloseableHttpClient;
+ import org.apache.http.impl.client.HttpClientBuilder;
+ import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
+
+ class Example {
+ CloseableHttpClient client() {
+ RequestConfig requestConfig = RequestConfig.custom().setStaleConnectionCheckEnabled(false).build();
+
+ PoolingHttpClientConnectionManager connManager = new PoolingHttpClientConnectionManager();
+
+ return HttpClientBuilder.create()
+ .setConnectionManager(connManager)
+ .setDefaultRequestConfig(requestConfig)
+ .build();
+ }
+ }
+ """,
+ """
+ import org.apache.hc.core5.util.TimeValue;
+ import org.apache.hc.client5.http.config.RequestConfig;
+ import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
+ import org.apache.hc.client5.http.impl.classic.HttpClientBuilder;
+ import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManager;
+
+ class Example {
+ CloseableHttpClient client() {
+ RequestConfig requestConfig = RequestConfig.custom().build();
+
+ PoolingHttpClientConnectionManager connManager = new PoolingHttpClientConnectionManager();
+
+ connManager.setValidateAfterInactivity(TimeValue.NEG_ONE_MILLISECOND);
+
+ return HttpClientBuilder.create()
+ .setConnectionManager(connManager)
+ .setDefaultRequestConfig(requestConfig)
+ .build();
+ }
+ }
+ """
+ )
+ );
+ }
+
+ // For `setStaleConnectionCheckEnabled(false)`, without an existing `connManager`, have to create one first, then call `setConnectionManager(connManager)`
+ @Test
+ void setStaleConnectionCheckEnabledFalseWithoutConnManager() {
+ rewriteRun(
+ // We call `setConnectionManager` on a HC4 client builder, with a HC5 connection manager argument
+ spec -> spec.afterTypeValidationOptions(TypeValidation.all().methodInvocations(false)),
+ //language=java
+ java(
+ """
+ import org.apache.http.client.config.RequestConfig;
+ import org.apache.http.impl.client.CloseableHttpClient;
+ import org.apache.http.impl.client.HttpClientBuilder;
+
+ class Example {
+ CloseableHttpClient client() {
+ RequestConfig requestConfig = RequestConfig.custom().setStaleConnectionCheckEnabled(false).build();
+
+ return HttpClientBuilder.create()
+ .setDefaultRequestConfig(requestConfig)
+ .build();
+ }
+ }
+ """,
+ """
+ import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManager;
+ import org.apache.hc.core5.util.TimeValue;
+ import org.apache.hc.client5.http.config.RequestConfig;
+ import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
+ import org.apache.hc.client5.http.impl.classic.HttpClientBuilder;
+
+ class Example {
+ CloseableHttpClient client() {
+ PoolingHttpClientConnectionManager poolingHttpClientConnectionManager = new PoolingHttpClientConnectionManager();
+ poolingHttpClientConnectionManager.setValidateAfterInactivity(TimeValue.NEG_ONE_MILLISECOND);
+ RequestConfig requestConfig = RequestConfig.custom().build();
+
+ return HttpClientBuilder.create()
+ .setDefaultRequestConfig(requestConfig).setConnectionManager(poolingHttpClientConnectionManager)
+ .build();
+ }
+ }
+ """
+ )
+ );
+ }
}