diff --git a/core/src/test/java/feign/RequestTemplateTest.java b/core/src/test/java/feign/RequestTemplateTest.java
index 9b700065e..c8d29a4b2 100644
--- a/core/src/test/java/feign/RequestTemplateTest.java
+++ b/core/src/test/java/feign/RequestTemplateTest.java
@@ -465,8 +465,8 @@ void fragmentShouldNotBeEncodedInTarget() {
@Test
void fragmentShouldBeExtractedWhenQueryParamsExist() {
RequestTemplate template =
- new RequestTemplate().method(HttpMethod.GET).uri("/path?query=queryValue#fragment",
- true);
+ new RequestTemplate().method(HttpMethod.GET).uri("/path?query=queryValue#fragment",
+ true);
assertThat(template.url()).isEqualTo("/path?query=queryValue#fragment");
assertThat(template.queryLine()).isEqualTo("?query=queryValue");
diff --git a/fastjson2/pom.xml b/fastjson2/pom.xml
index 68c6d1c9f..3000562f7 100644
--- a/fastjson2/pom.xml
+++ b/fastjson2/pom.xml
@@ -14,41 +14,39 @@
the License.
-->
-
- 4.0.0
-
-
- io.github.openfeign
- parent
- 13.3-SNAPSHOT
-
-
- feign-fastjson2
- Feign Fastjson2
- Feign Fastjson2
-
-
- ${project.basedir}/..
-
-
-
-
- ${project.groupId}
- feign-core
-
-
-
- com.alibaba.fastjson2
- fastjson2
-
-
-
- ${project.groupId}
- feign-core
- test-jar
- test
-
-
-
\ No newline at end of file
+
+ 4.0.0
+
+
+ io.github.openfeign
+ parent
+ 13.3-SNAPSHOT
+
+
+ feign-fastjson2
+ Feign Fastjson2
+ Feign Fastjson2
+
+
+ ${project.basedir}/..
+
+
+
+
+ ${project.groupId}
+ feign-core
+
+
+
+ com.alibaba.fastjson2
+ fastjson2
+
+
+
+ ${project.groupId}
+ feign-core
+ test-jar
+ test
+
+
+
diff --git a/jaxrs2/pom.xml b/jaxrs2/pom.xml
index d773086f7..497eae850 100644
--- a/jaxrs2/pom.xml
+++ b/jaxrs2/pom.xml
@@ -105,6 +105,16 @@
+
+ maven-jar-plugin
+
+
+
+ test-jar
+
+
+
+
org.eclipse.transformer
org.eclipse.transformer.maven
diff --git a/jaxrs2/src/main/java/feign/jaxrs2/JAXRS2Contract.java b/jaxrs2/src/main/java/feign/jaxrs2/JAXRS2Contract.java
index 8730a256b..a9646553a 100644
--- a/jaxrs2/src/main/java/feign/jaxrs2/JAXRS2Contract.java
+++ b/jaxrs2/src/main/java/feign/jaxrs2/JAXRS2Contract.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2012-2023 The Feign Authors
+ * Copyright 2012-2024 The Feign 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
@@ -13,13 +13,17 @@
*/
package feign.jaxrs2;
-import feign.jaxrs.JAXRSContract;
-import javax.ws.rs.*;
-import javax.ws.rs.container.Suspended;
-import javax.ws.rs.core.Context;
-import java.lang.reflect.Field;
import static feign.Util.checkState;
import static feign.Util.emptyToNull;
+import java.lang.reflect.Field;
+import javax.ws.rs.BeanParam;
+import javax.ws.rs.FormParam;
+import javax.ws.rs.HeaderParam;
+import javax.ws.rs.PathParam;
+import javax.ws.rs.QueryParam;
+import javax.ws.rs.container.Suspended;
+import javax.ws.rs.core.Context;
+import feign.jaxrs.JAXRSContract;
/**
* Please refer to the Feign
diff --git a/jaxrs2/src/main/java/feign/jaxrs2/JAXRSClient.java b/jaxrs2/src/main/java/feign/jaxrs2/JAXRSClient.java
index 17af624b8..943c0555a 100644
--- a/jaxrs2/src/main/java/feign/jaxrs2/JAXRSClient.java
+++ b/jaxrs2/src/main/java/feign/jaxrs2/JAXRSClient.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2012-2023 The Feign Authors
+ * Copyright 2012-2024 The Feign 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
@@ -23,7 +23,12 @@
import java.util.stream.Collectors;
import javax.ws.rs.client.ClientBuilder;
import javax.ws.rs.client.Entity;
-import javax.ws.rs.core.*;
+import javax.ws.rs.core.HttpHeaders;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.MultivaluedHashMap;
+import javax.ws.rs.core.MultivaluedMap;
+import javax.ws.rs.core.Response;
+import javax.ws.rs.core.Variant;
import feign.Client;
import feign.Request.Options;
diff --git a/jaxrs2/src/test/java/feign/jaxrs2/AbstractJAXRSClientTest.java b/jaxrs2/src/test/java/feign/jaxrs2/AbstractJAXRSClientTest.java
new file mode 100644
index 000000000..05fdf37d0
--- /dev/null
+++ b/jaxrs2/src/test/java/feign/jaxrs2/AbstractJAXRSClientTest.java
@@ -0,0 +1,162 @@
+/*
+ * Copyright 2012-2024 The Feign 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
+ *
+ * http://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 feign.jaxrs2;
+
+import static feign.Util.UTF_8;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.jupiter.api.Assumptions.assumeFalse;
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.util.Collections;
+import org.assertj.core.data.MapEntry;
+import org.junit.jupiter.api.Assumptions;
+import org.junit.jupiter.api.Test;
+import feign.Headers;
+import feign.RequestLine;
+import feign.Response;
+import feign.Util;
+import feign.assertj.MockWebServerAssertions;
+import feign.client.AbstractClientTest;
+import okhttp3.mockwebserver.MockResponse;
+
+public abstract class AbstractJAXRSClientTest extends AbstractClientTest {
+
+ @Override
+ public void patch() throws Exception {
+ try {
+ super.patch();
+ } catch (final RuntimeException e) {
+ Assumptions.assumeFalse(false, "JaxRS client do not support PATCH requests");
+ }
+ }
+
+ @Override
+ public void noResponseBodyForPut() throws Exception {
+ try {
+ super.noResponseBodyForPut();
+ } catch (final IllegalStateException e) {
+ Assumptions.assumeFalse(false, "JaxRS client do not support empty bodies on PUT");
+ }
+ }
+
+ @Override
+ public void noResponseBodyForPatch() {
+ try {
+ super.noResponseBodyForPatch();
+ } catch (final IllegalStateException e) {
+ Assumptions.assumeFalse(false, "JaxRS client do not support PATCH requests");
+ }
+ }
+
+ @Override
+ @Test
+ public void reasonPhraseIsOptional() throws IOException, InterruptedException {
+ server.enqueue(new MockResponse().setStatus("HTTP/1.1 " + 200));
+
+ final TestInterface api = newBuilder()
+ .target(TestInterface.class, "http://localhost:" + server.getPort());
+
+ final Response response = api.post("foo");
+
+ assertThat(response.status()).isEqualTo(200);
+ // jaxrsclient is creating a reason when none is present
+ // assertThat(response.reason()).isNullOrEmpty();
+ }
+
+ @Override
+ @Test
+ public void parsesRequestAndResponse() throws IOException, InterruptedException {
+ server.enqueue(new MockResponse().setBody("foo").addHeader("Foo: Bar"));
+
+ final TestInterface api = newBuilder()
+ .target(TestInterface.class, "http://localhost:" + server.getPort());
+
+ final Response response = api.post("foo");
+
+ assertThat(response.status()).isEqualTo(200);
+ assertThat(response.reason()).isEqualTo("OK");
+ assertThat(response.headers())
+ .hasEntrySatisfying("Content-Length", value -> {
+ assertThat(value).contains("3");
+ }).hasEntrySatisfying("Foo", value -> {
+ assertThat(value).contains("Bar");
+ });
+ assertThat(response.body().asInputStream())
+ .hasSameContentAs(new ByteArrayInputStream("foo".getBytes(UTF_8)));
+
+ /* queries with no values are omitted from the uri. See RFC 6750 */
+ MockWebServerAssertions.assertThat(server.takeRequest()).hasMethod("POST")
+ .hasPath("/?foo=bar&foo=baz&qux")
+ .hasBody("foo");
+ }
+
+ @Test
+ void contentTypeWithoutCharset2() throws Exception {
+ server.enqueue(new MockResponse()
+ .setBody("AAAAAAAA"));
+ final JaxRSClientTestInterface api = newBuilder()
+ .target(JaxRSClientTestInterface.class, "http://localhost:" + server.getPort());
+
+ final Response response = api.getWithContentType();
+ // Response length should not be null
+ assertThat(Util.toString(response.body().asReader(UTF_8))).isEqualTo("AAAAAAAA");
+
+ MockWebServerAssertions.assertThat(server.takeRequest())
+ .hasHeaders(
+ MapEntry.entry("Accept", Collections.singletonList("text/plain")),
+ MapEntry.entry("Content-Type", Collections.singletonList("text/plain")))
+ .hasMethod("GET");
+ }
+
+ /*
+ * JaxRS does not support gzip and deflate compression out-of-the-box.
+ */
+ @Override
+ public void canSupportGzip() throws Exception {
+ assumeFalse(false, "JaxRS client do not support gzip compression");
+ }
+
+ @Override
+ public void canSupportGzipOnError() throws Exception {
+ assumeFalse(false, "JaxRS client do not support gzip compression");
+ }
+
+ @Override
+ public void canSupportDeflate() throws Exception {
+ assumeFalse(false, "JaxRS client do not support deflate compression");
+ }
+
+ @Override
+ public void canSupportDeflateOnError() throws Exception {
+ assumeFalse(false, "JaxRS client do not support deflate compression");
+ }
+
+ @Override
+ public void canExceptCaseInsensitiveHeader() throws Exception {
+ assumeFalse(false, "JaxRS client do not support gzip compression");
+ }
+
+ public interface JaxRSClientTestInterface {
+
+ @RequestLine("GET /")
+ @Headers({"Accept: text/plain", "Content-Type: text/plain"})
+ Response getWithContentType();
+ }
+
+
+ @Override
+ public void veryLongResponseNullLength() {
+ assumeFalse(false, "JaxRS client hang if the response doesn't have a payload");
+ }
+}
diff --git a/jaxrs2/src/test/java/feign/jaxrs2/JAXRSClientTest.java b/jaxrs2/src/test/java/feign/jaxrs2/JAXRSClientTest.java
index 17083888b..95cded665 100644
--- a/jaxrs2/src/test/java/feign/jaxrs2/JAXRSClientTest.java
+++ b/jaxrs2/src/test/java/feign/jaxrs2/JAXRSClientTest.java
@@ -15,125 +15,32 @@
import static feign.Util.UTF_8;
import static org.assertj.core.api.Assertions.assertThat;
-import static org.junit.jupiter.api.Assumptions.assumeFalse;
-import java.io.ByteArrayInputStream;
-import java.io.IOException;
import java.util.Collections;
import javax.ws.rs.Consumes;
import javax.ws.rs.HeaderParam;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
-import javax.ws.rs.ProcessingException;
import org.assertj.core.data.MapEntry;
-import org.junit.jupiter.api.Assumptions;
import org.junit.jupiter.api.Test;
import feign.Feign;
import feign.Feign.Builder;
-import feign.Headers;
-import feign.RequestLine;
import feign.Response;
import feign.Util;
import feign.assertj.MockWebServerAssertions;
-import feign.client.AbstractClientTest;
import feign.jaxrs.JAXRSContract;
import okhttp3.mockwebserver.MockResponse;
/**
* Tests client-specific behavior, such as ensuring Content-Length is sent when specified.
*/
-public class JAXRSClientTest extends AbstractClientTest {
+public class JAXRSClientTest extends AbstractJAXRSClientTest {
@Override
public Builder newBuilder() {
return Feign.builder().client(new JAXRSClient());
}
- @Override
- public void patch() throws Exception {
- try {
- super.patch();
- } catch (final ProcessingException e) {
- Assumptions.assumeFalse(false, "JaxRS client do not support PATCH requests");
- }
- }
-
- @Override
- public void noResponseBodyForPut() throws Exception {
- try {
- super.noResponseBodyForPut();
- } catch (final IllegalStateException e) {
- Assumptions.assumeFalse(false, "JaxRS client do not support empty bodies on PUT");
- }
- }
-
- @Override
- public void noResponseBodyForPatch() {
- try {
- super.noResponseBodyForPatch();
- } catch (final IllegalStateException e) {
- Assumptions.assumeFalse(false, "JaxRS client do not support PATCH requests");
- }
- }
-
- @Override
- @Test
- public void reasonPhraseIsOptional() throws IOException, InterruptedException {
- server.enqueue(new MockResponse().setStatus("HTTP/1.1 " + 200));
-
- final TestInterface api = newBuilder()
- .target(TestInterface.class, "http://localhost:" + server.getPort());
-
- final Response response = api.post("foo");
-
- assertThat(response.status()).isEqualTo(200);
- // jaxrsclient is creating a reason when none is present
- // assertThat(response.reason()).isNullOrEmpty();
- }
-
- @Override
- @Test
- public void parsesRequestAndResponse() throws IOException, InterruptedException {
- server.enqueue(new MockResponse().setBody("foo").addHeader("Foo: Bar"));
-
- final TestInterface api = newBuilder()
- .target(TestInterface.class, "http://localhost:" + server.getPort());
-
- final Response response = api.post("foo");
-
- assertThat(response.status()).isEqualTo(200);
- assertThat(response.reason()).isEqualTo("OK");
- assertThat(response.headers())
- .hasEntrySatisfying("Content-Length", value -> {
- assertThat(value).contains("3");
- }).hasEntrySatisfying("Foo", value -> {
- assertThat(value).contains("Bar");
- });
- assertThat(response.body().asInputStream())
- .hasSameContentAs(new ByteArrayInputStream("foo".getBytes(UTF_8)));
-
- /* queries with no values are omitted from the uri. See RFC 6750 */
- MockWebServerAssertions.assertThat(server.takeRequest()).hasMethod("POST")
- .hasPath("/?foo=bar&foo=baz&qux")
- .hasBody("foo");
- }
-
- @Test
- void contentTypeWithoutCharset2() throws Exception {
- server.enqueue(new MockResponse()
- .setBody("AAAAAAAA"));
- final JaxRSClientTestInterface api = newBuilder()
- .target(JaxRSClientTestInterface.class, "http://localhost:" + server.getPort());
- final Response response = api.getWithContentType();
- // Response length should not be null
- assertThat(Util.toString(response.body().asReader(UTF_8))).isEqualTo("AAAAAAAA");
-
- MockWebServerAssertions.assertThat(server.takeRequest())
- .hasHeaders(
- MapEntry.entry("Accept", Collections.singletonList("text/plain")),
- MapEntry.entry("Content-Type", Collections.singletonList("text/plain")))
- .hasMethod("GET");
- }
@Test
void consumesMultipleWithContentTypeHeaderAndBody() throws Exception {
@@ -153,41 +60,6 @@ void consumesMultipleWithContentTypeHeaderAndBody() throws Exception {
.hasMethod("POST");
}
- /*
- * JaxRS does not support gzip and deflate compression out-of-the-box.
- */
- @Override
- public void canSupportGzip() throws Exception {
- assumeFalse(false, "JaxRS client do not support gzip compression");
- }
-
- @Override
- public void canSupportGzipOnError() throws Exception {
- assumeFalse(false, "JaxRS client do not support gzip compression");
- }
-
- @Override
- public void canSupportDeflate() throws Exception {
- assumeFalse(false, "JaxRS client do not support deflate compression");
- }
-
- @Override
- public void canSupportDeflateOnError() throws Exception {
- assumeFalse(false, "JaxRS client do not support deflate compression");
- }
-
- @Override
- public void canExceptCaseInsensitiveHeader() throws Exception {
- assumeFalse(false, "JaxRS client do not support gzip compression");
- }
-
- public interface JaxRSClientTestInterface {
-
- @RequestLine("GET /")
- @Headers({"Accept: text/plain", "Content-Type: text/plain"})
- Response getWithContentType();
- }
-
public interface JaxRSClientTestInterfaceWithJaxRsContract {
@Path("/")
@POST
@@ -195,9 +67,4 @@ public interface JaxRSClientTestInterfaceWithJaxRsContract {
Response consumesMultipleWithContentTypeHeaderAndBody(@HeaderParam("Content-Type") String contentType,
String body);
}
-
- @Override
- public void veryLongResponseNullLength() {
- assumeFalse(false, "JaxRS client hang if the response doesn't have a payload");
- }
}
diff --git a/jaxrs3/pom.xml b/jaxrs3/pom.xml
index fac192d7e..3683dc085 100644
--- a/jaxrs3/pom.xml
+++ b/jaxrs3/pom.xml
@@ -29,6 +29,7 @@
11
+ 3.1.6
${project.basedir}/..
@@ -67,7 +68,6 @@
${project.groupId}
feign-jaxrs
test-jar
- test
javax.ws.rs
@@ -75,6 +75,30 @@
+
+
+ ${project.groupId}
+ feign-jaxrs2
+ test-jar
+
+
+ com.squareup.okhttp3
+ mockwebserver
+ test
+
+
+ org.glassfish.jersey.core
+ jersey-client
+ ${jersey.version}
+ test
+
+
+ org.glassfish.jersey.inject
+ jersey-hk2
+ ${jersey.version}
+ test
+
+
diff --git a/jaxrs3/src/main/java/feign/jaxrs3/JAXRS3Contract.java b/jaxrs3/src/main/java/feign/jaxrs3/JAXRS3Contract.java
index 60083e07d..333b6ad3e 100644
--- a/jaxrs3/src/main/java/feign/jaxrs3/JAXRS3Contract.java
+++ b/jaxrs3/src/main/java/feign/jaxrs3/JAXRS3Contract.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2012-2023 The Feign Authors
+ * Copyright 2012-2024 The Feign 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
@@ -13,27 +13,7 @@
*/
package feign.jaxrs3;
-import static feign.Util.checkState;
-import static feign.Util.emptyToNull;
-import static feign.Util.removeValues;
-import java.lang.annotation.Annotation;
-import java.lang.reflect.Field;
-import java.lang.reflect.Method;
-import java.util.Collections;
-import feign.MethodMetadata;
-import feign.Request;
import feign.jaxrs2.JAXRS2Contract;
-import jakarta.ws.rs.BeanParam;
-import jakarta.ws.rs.Consumes;
-import jakarta.ws.rs.FormParam;
-import jakarta.ws.rs.HeaderParam;
-import jakarta.ws.rs.HttpMethod;
-import jakarta.ws.rs.Path;
-import jakarta.ws.rs.PathParam;
-import jakarta.ws.rs.Produces;
-import jakarta.ws.rs.QueryParam;
-import jakarta.ws.rs.container.Suspended;
-import jakarta.ws.rs.core.Context;
public class JAXRS3Contract extends JAXRS2Contract {
}
diff --git a/jaxrs3/src/test/java/feign/jaxrs3/JAXRS3ClientTest.java b/jaxrs3/src/test/java/feign/jaxrs3/JAXRS3ClientTest.java
new file mode 100644
index 000000000..88cdfb138
--- /dev/null
+++ b/jaxrs3/src/test/java/feign/jaxrs3/JAXRS3ClientTest.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright 2012-2024 The Feign 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
+ *
+ * http://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 feign.jaxrs3;
+
+import feign.Feign;
+import feign.Feign.Builder;
+import feign.jaxrs2.AbstractJAXRSClientTest;
+
+/**
+ * Tests client-specific behavior, such as ensuring Content-Length is sent when specified.
+ */
+public class JAXRS3ClientTest extends AbstractJAXRSClientTest {
+
+ @Override
+ public Builder newBuilder() {
+ return Feign.builder().client(new JAXRS3Client());
+ }
+
+}
diff --git a/jaxrs4/README.md b/jaxrs4/README.md
new file mode 100644
index 000000000..ddda3b9f8
--- /dev/null
+++ b/jaxrs4/README.md
@@ -0,0 +1,5 @@
+# Feign Jakarta JaxRS 4
+
+Feign Jakarta JaxRS 4 module is dedicated to testing the compatibility of the JaxRS 3 module with JaxRS 4 to ensure seamless integration with the latest version.
+
+A separated module is required due java version requirement.
diff --git a/jaxrs4/pom.xml b/jaxrs4/pom.xml
new file mode 100644
index 000000000..a6408d238
--- /dev/null
+++ b/jaxrs4/pom.xml
@@ -0,0 +1,121 @@
+
+
+
+ 4.0.0
+
+
+ io.github.openfeign
+ parent
+ 13.3-SNAPSHOT
+
+
+ feign-jaxrs4
+ Feign JAXRS 4
+ Feign JAXRS 4
+
+
+ 17
+ 4.0.0-M1
+ ${project.basedir}/..
+
+
+
+
+ ${project.groupId}
+ feign-jaxrs3
+
+
+
+ jakarta.ws.rs
+ jakarta.ws.rs-api
+ 4.0.0
+
+
+
+
+ ${project.groupId}
+ feign-gson
+ test
+
+
+
+ ${project.groupId}
+ feign-core
+ test-jar
+ test
+
+
+ ${project.groupId}
+ feign-jaxrs
+ test-jar
+
+
+ javax.ws.rs
+ jsr311-api
+
+
+
+
+
+ ${project.groupId}
+ feign-jaxrs2
+ test-jar
+
+
+ com.squareup.okhttp3
+ mockwebserver
+ test
+
+
+ org.glassfish.jersey.core
+ jersey-client
+ ${jersey.version}
+ test
+
+
+ org.glassfish.jersey.inject
+ jersey-hk2
+ ${jersey.version}
+ test
+
+
+
+
+
+
+ org.moditect
+ moditect-maven-plugin
+
+ true
+
+
+ !jsr311.api;
+ !feign.jaxrs;
+ !feign.jaxrs2;
+ *;
+ !jsr311.api;
+ !feign.jaxrs;
+ !feign.jaxrs2;
+ *;
+ true
+
+
+
+
+
+
+
diff --git a/jaxrs4/src/test/java/feign/jaxrs/JAXRS4ContractTest.java b/jaxrs4/src/test/java/feign/jaxrs/JAXRS4ContractTest.java
new file mode 100644
index 000000000..a2eae360a
--- /dev/null
+++ b/jaxrs4/src/test/java/feign/jaxrs/JAXRS4ContractTest.java
@@ -0,0 +1,466 @@
+/*
+ * Copyright 2012-2024 The Feign 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
+ *
+ * http://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 feign.jaxrs;
+
+import static feign.assertj.FeignAssertions.assertThat;
+import static java.util.Arrays.asList;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.data.MapEntry.entry;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+import java.net.URI;
+import java.util.List;
+import org.junit.jupiter.api.Test;
+import feign.MethodMetadata;
+import feign.Response;
+import feign.jaxrs3.JAXRS3Contract;
+import feign.jaxrs.JAXRS4ContractTest.JakartaInternals.BeanParamInput;
+import jakarta.ws.rs.BeanParam;
+import jakarta.ws.rs.Consumes;
+import jakarta.ws.rs.DELETE;
+import jakarta.ws.rs.FormParam;
+import jakarta.ws.rs.GET;
+import jakarta.ws.rs.HeaderParam;
+import jakarta.ws.rs.HttpMethod;
+import jakarta.ws.rs.POST;
+import jakarta.ws.rs.PUT;
+import jakarta.ws.rs.Path;
+import jakarta.ws.rs.PathParam;
+import jakarta.ws.rs.Produces;
+import jakarta.ws.rs.QueryParam;
+import jakarta.ws.rs.container.AsyncResponse;
+import jakarta.ws.rs.container.Suspended;
+import jakarta.ws.rs.core.Context;
+import jakarta.ws.rs.core.MediaType;
+import jakarta.ws.rs.core.UriInfo;
+
+/**
+ * Tests interfaces defined per {@link JAXRS3Contract} are interpreted into expected
+ * {@link feign .RequestTemplate template} instances.
+ */
+class JAXRS4ContractTest extends JAXRSContractTestSupport {
+
+ @Test
+ void injectJaxrsInternals() throws Exception {
+ final MethodMetadata methodMetadata =
+ parseAndValidateMetadata(JakartaInternals.class, "inject", AsyncResponse.class,
+ UriInfo.class);
+ assertThat(methodMetadata.template())
+ .noRequestBody();
+ }
+
+ @Test
+ void injectBeanParam() throws Exception {
+ final MethodMetadata methodMetadata =
+ parseAndValidateMetadata(JakartaInternals.class, "beanParameters", BeanParamInput.class);
+ assertThat(methodMetadata.template())
+ .noRequestBody();
+
+ assertThat(methodMetadata.template())
+ .hasHeaders(entry("X-Custom-Header", asList("{X-Custom-Header}")));
+ assertThat(methodMetadata.template())
+ .hasQueries(entry("query", asList("{query}")));
+ assertThat(methodMetadata.formParams())
+ .isNotEmpty()
+ .containsExactly("form");
+
+ }
+
+ public interface JakartaInternals {
+ @GET
+ @Path("/")
+ void inject(@Suspended AsyncResponse ar, @Context UriInfo info);
+
+ @Path("/{path}")
+ @POST
+ void beanParameters(@BeanParam BeanParamInput beanParam);
+
+ public class BeanParamInput {
+
+ @PathParam("path")
+ String path;
+
+ @QueryParam("query")
+ String query;
+
+ @FormParam("form")
+ String form;
+
+ @HeaderParam("X-Custom-Header")
+ String header;
+ }
+ }
+
+ interface Methods {
+
+ @POST
+ void post();
+
+ @PUT
+ void put();
+
+ @GET
+ void get();
+
+ @DELETE
+ void delete();
+ }
+
+ interface CustomMethod {
+
+ @PATCH
+ Response patch();
+
+ @Target({ElementType.METHOD})
+ @Retention(RetentionPolicy.RUNTIME)
+ @HttpMethod("PATCH")
+ public @interface PATCH {
+ }
+ }
+
+ interface WithQueryParamsInPath {
+
+ @GET
+ @Path("/")
+ Response none();
+
+ @GET
+ @Path("/?Action=GetUser")
+ Response one();
+
+ @GET
+ @Path("/?Action=GetUser&Version=2010-05-08")
+ Response two();
+
+ @GET
+ @Path("/?Action=GetUser&Version=2010-05-08&limit=1")
+ Response three();
+
+ @GET
+ @Path("/?flag&Action=GetUser&Version=2010-05-08")
+ Response empty();
+ }
+
+ @Consumes(MediaType.APPLICATION_JSON)
+ @Produces(MediaType.TEXT_HTML)
+ interface ProducesAndConsumes {
+
+ @GET
+ @Produces("application/xml")
+ Response produces();
+
+ @GET
+ @Produces({"application/xml", "text/plain"})
+ Response producesMultiple();
+
+ @GET
+ @Produces({})
+ Response producesNada();
+
+ @GET
+ @Produces({""})
+ Response producesEmpty();
+
+ @POST
+ @Consumes("application/xml")
+ Response consumes();
+
+ @POST
+ @Consumes({"application/xml", "application/json"})
+ Response consumesMultiple();
+
+ @POST
+ @Consumes({})
+ Response consumesNada();
+
+ @POST
+ @Consumes({""})
+ Response consumesEmpty();
+
+ @POST
+ Response producesAndConsumes();
+ }
+
+ interface BodyParams {
+
+ @POST
+ Response post(List body);
+
+ @POST
+ Response tooMany(List body, List body2);
+ }
+
+ @Path("")
+ interface EmptyPathOnType {
+
+ @GET
+ Response base();
+
+ @GET
+ @Path("/specific")
+ Response get();
+ }
+
+ @Path("/base")
+ interface PathOnType {
+
+ @GET
+ Response base();
+
+ @GET
+ @Path("/specific")
+ Response get();
+
+ @GET
+ @Path("")
+ Response emptyPath();
+
+ @GET
+ @Path("/{param}")
+ Response emptyPathParam(@PathParam(value = "") String empty);
+
+ @GET
+ @Path("/{ param }")
+ Response pathParamWithSpaces(@PathParam("param") String path);
+
+ @GET
+ @Path("regex/{param:.+}")
+ Response pathParamWithRegex(@PathParam("param") String path);
+
+ @GET
+ @Path("regex/{param1:[0-9]*}/{ param2 : .+}")
+ Response pathParamWithMultipleRegex(
+ @PathParam("param1") String param1,
+ @PathParam("param2") String param2);
+ }
+
+ @Path("/{baseparam: [0-9]+}")
+ interface ComplexPathOnType {
+
+ @GET
+ @Path("regex/{param1:[0-9]*}/{ param2 : .+}")
+ Response pathParamWithMultipleRegex(
+ @PathParam("param1") String param1,
+ @PathParam("param2") String param2);
+ }
+
+ interface WithURIParam {
+
+ @GET
+ @Path("/{1}/{2}")
+ Response uriParam(@PathParam("1") String one, URI endpoint, @PathParam("2") String two);
+ }
+
+ interface WithPathAndQueryParams {
+
+ @GET
+ @Path("/domains/{domainId}/records")
+ Response recordsByNameAndType(
+ @PathParam("domainId") int id,
+ @QueryParam("name") String nameFilter,
+ @QueryParam("type") String typeFilter);
+
+ @GET
+ Response empty(@QueryParam("") String empty);
+ }
+
+ interface FormParams {
+
+ @POST
+ void login(
+ @FormParam("customer_name") String customer,
+ @FormParam("user_name") String user,
+ @FormParam("password") String password);
+
+ @GET
+ Response emptyFormParam(@FormParam("") String empty);
+ }
+
+ interface HeaderParams {
+
+ @POST
+ void logout(@HeaderParam("Auth-Token") String token);
+
+ @GET
+ Response emptyHeaderParam(@HeaderParam("") String empty);
+ }
+
+ @Path("base")
+ interface PathsWithoutAnySlashes {
+
+ @GET
+ @Path("specific")
+ Response get();
+ }
+
+ @Path("/base")
+ interface PathsWithSomeSlashes {
+
+ @GET
+ @Path("specific")
+ Response get();
+ }
+
+ @Path("base")
+ interface PathsWithSomeOtherSlashes {
+
+ @GET
+ @Path("/specific")
+ Response get();
+ }
+
+ @Path("/")
+ interface ClassRootPath {
+ @GET
+ @Path("/specific")
+ Response get();
+ }
+
+ @Path("/base/")
+ interface ClassPathWithTrailingSlash {
+ @GET
+ @Path("/specific")
+ Response get();
+ }
+
+ @Path("/base/")
+ interface MethodWithFirstPathThenGetWithoutLeadingSlash {
+ @Path("specific")
+ @GET
+ Response get();
+ }
+
+ interface MixedAnnotations {
+
+ @GET
+ @Path("/api/stuff?multiple=stuff")
+ @Produces("application/json")
+ Response getWithHeaders(
+ @HeaderParam("Accept") String accept,
+ @QueryParam("multiple") String multiple,
+ @QueryParam("another") String another);
+ }
+
+ @Override
+ protected JAXRS3Contract createContract() {
+ return new JAXRS3Contract();
+ }
+
+ @Override
+ protected MethodMetadata parseAndValidateMetadata(
+ Class> targetType,
+ String method,
+ Class>... parameterTypes)
+ throws NoSuchMethodException {
+ return contract.parseAndValidateMetadata(
+ targetType, targetType.getMethod(method, parameterTypes));
+ }
+
+ @Override
+ protected Class> methodsClass() {
+ return Methods.class;
+ }
+
+ @Override
+ protected Class> customMethodClass() {
+ return CustomMethod.class;
+ }
+
+ @Override
+ protected Class> withQueryParamsInPathClass() {
+ return WithQueryParamsInPath.class;
+ }
+
+ @Override
+ protected Class> producesAndConsumesClass() {
+ return ProducesAndConsumes.class;
+ }
+
+ @Override
+ protected Class> bodyParamsClass() {
+ return BodyParams.class;
+ }
+
+ @Override
+ protected Class> emptyPathOnTypeClass() {
+ return EmptyPathOnType.class;
+ }
+
+ @Override
+ protected Class> pathOnTypeClass() {
+ return PathOnType.class;
+ }
+
+ @Override
+ protected Class> complexPathOnTypeClass() {
+ return ComplexPathOnType.class;
+ }
+
+ @Override
+ protected Class> withURIParamClass() {
+ return WithURIParam.class;
+ }
+
+ @Override
+ protected Class> withPathAndQueryParamsClass() {
+ return WithPathAndQueryParams.class;
+ }
+
+ @Override
+ protected Class> formParamsClass() {
+ return FormParams.class;
+ }
+
+ @Override
+ protected Class> headerParamsClass() {
+ return HeaderParams.class;
+ }
+
+ @Override
+ protected Class> pathsWithoutAnySlashesClass() {
+ return PathsWithoutAnySlashes.class;
+ }
+
+ @Override
+ protected Class> pathsWithSomeSlashesClass() {
+ return PathsWithSomeSlashes.class;
+ }
+
+ @Override
+ protected Class> pathsWithSomeOtherSlashesClass() {
+ return PathsWithSomeOtherSlashes.class;
+ }
+
+ @Override
+ protected Class> classRootPathClass() {
+ return ClassRootPath.class;
+ }
+
+ @Override
+ protected Class> classPathWithTrailingSlashClass() {
+ return ClassPathWithTrailingSlash.class;
+ }
+
+ @Override
+ protected Class> methodWithFirstPathThenGetWithoutLeadingSlashClass() {
+ return MethodWithFirstPathThenGetWithoutLeadingSlash.class;
+ }
+
+ @Override
+ protected Class> mixedAnnotationsClass() {
+ return MixedAnnotations.class;
+ }
+}
diff --git a/jaxrs4/src/test/java/feign/jaxrs4/JAXRS4ClientTest.java b/jaxrs4/src/test/java/feign/jaxrs4/JAXRS4ClientTest.java
new file mode 100644
index 000000000..8e7a556db
--- /dev/null
+++ b/jaxrs4/src/test/java/feign/jaxrs4/JAXRS4ClientTest.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright 2012-2024 The Feign 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
+ *
+ * http://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 feign.jaxrs4;
+
+import feign.Feign;
+import feign.Feign.Builder;
+import feign.jaxrs2.AbstractJAXRSClientTest;
+import feign.jaxrs3.JAXRS3Client;
+
+/**
+ * Tests client-specific behavior, such as ensuring Content-Length is sent when specified.
+ */
+public class JAXRS4ClientTest extends AbstractJAXRSClientTest {
+
+ @Override
+ public Builder newBuilder() {
+ return Feign.builder().client(new JAXRS3Client());
+ }
+
+}
diff --git a/pom.xml b/pom.xml
index 032ce78b4..d34a490aa 100644
--- a/pom.xml
+++ b/pom.xml
@@ -39,6 +39,7 @@
jaxrs
jaxrs2
jaxrs3
+ jaxrs4
java11
jakarta
json
@@ -257,6 +258,14 @@
${project.version}
+
+ ${project.groupId}
+ feign-jaxrs2
+ ${project.version}
+ test-jar
+ test
+
+
${project.groupId}
feign-jaxrs2