[Java][client] Fix feign classcastexception when getting headers (#16745)

* Avoid ClassCastException when getting headers (the header values are Collection<String> and not List<String>)
Delete unused classes

* Remove feign10x

* Add unit test
Refactor to avoid creating the headers map when ApiResponse is not used
This commit is contained in:
Robert Danci 2023-10-10 15:53:32 +09:00 committed by GitHub
parent 494ee489ad
commit 87f9d53c3a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 89 additions and 107 deletions

6
.gitignore vendored
View File

@ -49,8 +49,6 @@ nb-configuration.xml
/target
/generated-files
test-output/
nbactions.xml
test-output/
# website
website/build/
@ -73,7 +71,6 @@ samples/client/petstore/build
samples/client/petstore/cpp-qt/PetStore/moc_*
samples/client/petstore/cpp-qt/PetStore/*.o
samples/client/petstore/cpp-qt/build-*
samples/client/petstore/cpp-qt/build-*
samples/client/petstore/cpp-qt/PetStore/PetStore
samples/client/petstore/cpp-qt/PetStore/Makefile
samples/client/petstore/cpp-qt/PetStore/PetStore.pro.user
@ -100,9 +97,7 @@ samples/client/petstore/java/jersey2/build/
samples/client/petstore/java/okhttp-gson/.gradle/
samples/client/petstore/java/okhttp-gson/build/
samples/client/petstore/java/feign/build/
samples/client/petstore/java/feign10x/build/
samples/client/petstore/java/feign/project/
samples/client/petstore/java/feign10x/project/
samples/client/petstore/java/retrofit/build/
samples/client/petstore/java/retrofit2/build/
samples/client/petstore/java/retrofit2/hello.txt
@ -110,7 +105,6 @@ samples/client/petstore/java/retrofit2rx/build/
samples/client/petstore/java/default/build/
samples/client/petstore/scala/build/
samples/client/petstore/java/resttemplate/hello.txt
samples/client/petstore/java/retrofit2/hello.txt
samples/client/petstore/java/feign/hello.txt
samples/client/petstore/java/jersey2-java6/project/
samples/client/petstore/java/jersey2-java8/project/

View File

@ -22,14 +22,13 @@ public class ApiResponseDecoder extends JacksonDecoder {
@Override
public Object decode(Response response, Type type) throws IOException {
Map<String, Collection<String>> responseHeaders = Collections.unmodifiableMap(response.headers());
//Detects if the type is an instance of the parameterized class ApiResponse
Type responseBodyType;
if (type instanceof ParameterizedType && Types.getRawType(type).isAssignableFrom(ApiResponse.class)) {
//The ApiResponse class has a single type parameter, the Dto class itself
responseBodyType = ((ParameterizedType) type).getActualTypeArguments()[0];
Type responseBodyType = ((ParameterizedType) type).getActualTypeArguments()[0];
Object body = super.decode(response, responseBodyType);
return new ApiResponse(response.status(), responseHeaders, body);
Map<String, Collection<String>> responseHeaders = Collections.unmodifiableMap(response.headers());
return new ApiResponse<>(response.status(), responseHeaders, body);
} else {
//The response is not encapsulated in the ApiResponse, decode the Dto as normal
return super.decode(response, type);

View File

@ -1,19 +1,19 @@
package {{modelPackage}};
import java.util.Map;
import java.util.List;
import java.util.Collection;
public class ApiResponse<T>{
final private int statusCode;
final private Map<String, List<String>> headers;
final private Map<String, Collection<String>> headers;
final private T data;
/**
* @param statusCode The status code of HTTP response
* @param headers The headers of HTTP response
*/
public ApiResponse(int statusCode, Map<String, List<String>> headers) {
public ApiResponse(int statusCode, Map<String, Collection<String>> headers) {
this(statusCode, headers, null);
}
@ -22,7 +22,7 @@ public class ApiResponse<T>{
* @param headers The headers of HTTP response
* @param data The object deserialized from response bod
*/
public ApiResponse(int statusCode, Map<String, List<String>> headers, T data) {
public ApiResponse(int statusCode, Map<String, Collection<String>> headers, T data) {
this.statusCode = statusCode;
this.headers = headers;
this.data = data;
@ -32,7 +32,7 @@ public class ApiResponse<T>{
return statusCode;
}
public Map<String, List<String>> getHeaders() {
public Map<String, Collection<String>> getHeaders() {
return headers;
}

View File

@ -1,19 +1,19 @@
package org.openapitools.client.model;
import java.util.Map;
import java.util.List;
import java.util.Collection;
public class ApiResponse<T>{
final private int statusCode;
final private Map<String, List<String>> headers;
final private Map<String, Collection<String>> headers;
final private T data;
/**
* @param statusCode The status code of HTTP response
* @param headers The headers of HTTP response
*/
public ApiResponse(int statusCode, Map<String, List<String>> headers) {
public ApiResponse(int statusCode, Map<String, Collection<String>> headers) {
this(statusCode, headers, null);
}
@ -22,7 +22,7 @@ public class ApiResponse<T>{
* @param headers The headers of HTTP response
* @param data The object deserialized from response bod
*/
public ApiResponse(int statusCode, Map<String, List<String>> headers, T data) {
public ApiResponse(int statusCode, Map<String, Collection<String>> headers, T data) {
this.statusCode = statusCode;
this.headers = headers;
this.data = data;
@ -32,7 +32,7 @@ public class ApiResponse<T>{
return statusCode;
}
public Map<String, List<String>> getHeaders() {
public Map<String, Collection<String>> getHeaders() {
return headers;
}

View File

@ -22,14 +22,13 @@ public class ApiResponseDecoder extends JacksonDecoder {
@Override
public Object decode(Response response, Type type) throws IOException {
Map<String, Collection<String>> responseHeaders = Collections.unmodifiableMap(response.headers());
//Detects if the type is an instance of the parameterized class ApiResponse
Type responseBodyType;
if (type instanceof ParameterizedType && Types.getRawType(type).isAssignableFrom(ApiResponse.class)) {
//The ApiResponse class has a single type parameter, the Dto class itself
responseBodyType = ((ParameterizedType) type).getActualTypeArguments()[0];
Type responseBodyType = ((ParameterizedType) type).getActualTypeArguments()[0];
Object body = super.decode(response, responseBodyType);
return new ApiResponse(response.status(), responseHeaders, body);
Map<String, Collection<String>> responseHeaders = Collections.unmodifiableMap(response.headers());
return new ApiResponse<>(response.status(), responseHeaders, body);
} else {
//The response is not encapsulated in the ApiResponse, decode the Dto as normal
return super.decode(response, type);

View File

@ -1,38 +0,0 @@
package org.openapitools.client;
import com.fasterxml.jackson.databind.ObjectMapper;
import feign.Response;
import feign.Types;
import feign.jackson.JacksonDecoder;
import java.io.IOException;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.Collection;
import java.util.Collections;
import java.util.Map;
import org.openapitools.client.model.HttpResponse;
public class JacksonResponseDecoder extends JacksonDecoder {
public JacksonResponseDecoder(ObjectMapper mapper) {
super(mapper);
}
@Override
public Object decode(Response response, Type type) throws IOException {
Map<String, Collection<String>> responseHeaders = Collections.unmodifiableMap(response.headers());
//Detects if the type is an instance of the parameterized class HttpResponse
Type responseBodyType;
if (Types.getRawType(type).isAssignableFrom(HttpResponse.class)) {
//The HttpResponse class has a single type parameter, the Dto class itself
responseBodyType = ((ParameterizedType) type).getActualTypeArguments()[0];
Object body = super.decode(response, responseBodyType);
return new HttpResponse(responseHeaders, body, response.status());
} else {
//The response is not encapsulated in the HttpResponse, decode the Dto as normal
return super.decode(response, type);
}
}
}

View File

@ -1,19 +1,19 @@
package org.openapitools.client.model;
import java.util.Map;
import java.util.List;
import java.util.Collection;
public class ApiResponse<T>{
final private int statusCode;
final private Map<String, List<String>> headers;
final private Map<String, Collection<String>> headers;
final private T data;
/**
* @param statusCode The status code of HTTP response
* @param headers The headers of HTTP response
*/
public ApiResponse(int statusCode, Map<String, List<String>> headers) {
public ApiResponse(int statusCode, Map<String, Collection<String>> headers) {
this(statusCode, headers, null);
}
@ -22,7 +22,7 @@ public class ApiResponse<T>{
* @param headers The headers of HTTP response
* @param data The object deserialized from response bod
*/
public ApiResponse(int statusCode, Map<String, List<String>> headers, T data) {
public ApiResponse(int statusCode, Map<String, Collection<String>> headers, T data) {
this.statusCode = statusCode;
this.headers = headers;
this.data = data;
@ -32,7 +32,7 @@ public class ApiResponse<T>{
return statusCode;
}
public Map<String, List<String>> getHeaders() {
public Map<String, Collection<String>> getHeaders() {
return headers;
}

View File

@ -1,31 +0,0 @@
package org.openapitools.client.model;
import java.util.Map;
import java.util.Collection;
public class HttpResponse<T>{
private Map<String, Collection<String>> headers;
private T body;
private int status;
public HttpResponse(Map<String, Collection<String>> headers, T body, int status) {
this.headers = headers;
this.body = body;
this.status = status;
}
public T getBody(){
return body;
}
public Map<String, Collection<String>> getHeaders(){
return headers;
}
public int getStatus(){
return status;
}
}

View File

@ -22,14 +22,13 @@ public class ApiResponseDecoder extends JacksonDecoder {
@Override
public Object decode(Response response, Type type) throws IOException {
Map<String, Collection<String>> responseHeaders = Collections.unmodifiableMap(response.headers());
//Detects if the type is an instance of the parameterized class ApiResponse
Type responseBodyType;
if (type instanceof ParameterizedType && Types.getRawType(type).isAssignableFrom(ApiResponse.class)) {
//The ApiResponse class has a single type parameter, the Dto class itself
responseBodyType = ((ParameterizedType) type).getActualTypeArguments()[0];
Type responseBodyType = ((ParameterizedType) type).getActualTypeArguments()[0];
Object body = super.decode(response, responseBodyType);
return new ApiResponse(response.status(), responseHeaders, body);
Map<String, Collection<String>> responseHeaders = Collections.unmodifiableMap(response.headers());
return new ApiResponse<>(response.status(), responseHeaders, body);
} else {
//The response is not encapsulated in the ApiResponse, decode the Dto as normal
return super.decode(response, type);

View File

@ -1,19 +1,19 @@
package org.openapitools.client.model;
import java.util.Map;
import java.util.List;
import java.util.Collection;
public class ApiResponse<T>{
final private int statusCode;
final private Map<String, List<String>> headers;
final private Map<String, Collection<String>> headers;
final private T data;
/**
* @param statusCode The status code of HTTP response
* @param headers The headers of HTTP response
*/
public ApiResponse(int statusCode, Map<String, List<String>> headers) {
public ApiResponse(int statusCode, Map<String, Collection<String>> headers) {
this(statusCode, headers, null);
}
@ -22,7 +22,7 @@ public class ApiResponse<T>{
* @param headers The headers of HTTP response
* @param data The object deserialized from response bod
*/
public ApiResponse(int statusCode, Map<String, List<String>> headers, T data) {
public ApiResponse(int statusCode, Map<String, Collection<String>> headers, T data) {
this.statusCode = statusCode;
this.headers = headers;
this.data = data;
@ -32,7 +32,7 @@ public class ApiResponse<T>{
return statusCode;
}
public Map<String, List<String>> getHeaders() {
public Map<String, Collection<String>> getHeaders() {
return headers;
}

View File

@ -0,0 +1,60 @@
package org.openapitools.client;
import static org.junit.jupiter.api.Assertions.assertEquals;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.collect.ImmutableMap;
import feign.Request;
import feign.Request.HttpMethod;
import feign.RequestTemplate;
import feign.Response;
import java.io.IOException;
import java.util.Collection;
import java.util.Collections;
import org.junit.jupiter.api.Test;
import org.openapitools.client.model.ApiResponse;
import org.openapitools.client.model.Cat;
class ApiResponseDecoderTest {
private final ObjectMapper objectMapper = new ObjectMapper();
ApiResponseDecoder decoder = new ApiResponseDecoder(objectMapper);
@Test
void shouldDecodeApiResponseWithHeaders() throws IOException {
final Cat cat = makeCat();
TypeReference<ApiResponse<Cat>> typeReference = new TypeReference<ApiResponse<Cat>>() {
};
ApiResponse<Cat> response = (ApiResponse<Cat>) decoder.decode(Response.builder()
.headers(ImmutableMap.of("Location",
Collections.singletonList("https://example.com/cats/1")))
.body(objectMapper.writeValueAsBytes(cat))
.status(201)
.request(buildRequest())
.build(), typeReference.getType());
assertEquals(cat.getColor(), response.getData().getColor());
assertEquals(cat.isDeclawed(), response.getData().isDeclawed());
Collection<String> locationValues = response.getHeaders().get("Location");
assertEquals(1, locationValues.size());
assertEquals("https://example.com/cats/1", locationValues.iterator().next());
}
private Cat makeCat() {
final Cat cat = new Cat();
cat.color("black");
cat.declawed(true);
return cat;
}
private Request buildRequest() {
return Request.create(HttpMethod.GET, "https://example.com/cats/1", Collections.emptyMap(),
null, new RequestTemplate());
}
}