|
18 | 18 | import feign.Types; |
19 | 19 | import feign.jackson.JacksonDecoder; |
20 | 20 |
|
| 21 | +import java.io.File; |
21 | 22 | import java.io.IOException; |
| 23 | +import java.io.InputStream; |
22 | 24 | import java.lang.reflect.ParameterizedType; |
23 | 25 | import java.lang.reflect.Type; |
| 26 | +import java.nio.file.Files; |
| 27 | +import java.nio.file.Paths; |
| 28 | +import java.nio.file.StandardCopyOption; |
24 | 29 | import java.util.Collection; |
25 | 30 | import java.util.Collections; |
26 | 31 | import java.util.Map; |
| 32 | +import java.util.regex.Matcher; |
| 33 | +import java.util.regex.Pattern; |
27 | 34 |
|
28 | 35 | import org.openapitools.client.model.ApiResponse; |
29 | 36 |
|
30 | 37 | public class ApiResponseDecoder extends JacksonDecoder { |
31 | 38 |
|
| 39 | + private static final Pattern FILENAME_PATTERN = |
| 40 | + Pattern.compile("filename=\"([^\"]+)\"|filename=([^\\s;]+)"); |
| 41 | + |
32 | 42 | public ApiResponseDecoder(ObjectMapper mapper) { |
33 | 43 | super(mapper); |
34 | 44 | } |
35 | 45 |
|
36 | 46 | @Override |
37 | 47 | public Object decode(Response response, Type type) throws IOException { |
38 | | - //Detects if the type is an instance of the parameterized class ApiResponse |
39 | 48 | if (type instanceof ParameterizedType && Types.getRawType(type).isAssignableFrom(ApiResponse.class)) { |
40 | | - //The ApiResponse class has a single type parameter, the Dto class itself |
41 | 49 | Type responseBodyType = ((ParameterizedType) type).getActualTypeArguments()[0]; |
42 | | - Object body = super.decode(response, responseBodyType); |
| 50 | + Object body = isBinaryType(responseBodyType) |
| 51 | + ? decodeBinary(response, responseBodyType) |
| 52 | + : super.decode(response, responseBodyType); |
43 | 53 | Map<String, Collection<String>> responseHeaders = Collections.unmodifiableMap(response.headers()); |
44 | 54 | return new ApiResponse<>(response.status(), responseHeaders, body); |
| 55 | + } |
| 56 | + |
| 57 | + if (isBinaryType(type)) { |
| 58 | + return decodeBinary(response, type); |
| 59 | + } |
| 60 | + |
| 61 | + return super.decode(response, type); |
| 62 | + } |
| 63 | + |
| 64 | + private boolean isBinaryType(Type type) { |
| 65 | + Class<?> raw = Types.getRawType(type); |
| 66 | + return File.class.isAssignableFrom(raw) |
| 67 | + || byte[].class.isAssignableFrom(raw) |
| 68 | + || InputStream.class.isAssignableFrom(raw); |
| 69 | + } |
| 70 | + |
| 71 | + private Object decodeBinary(Response response, Type type) throws IOException { |
| 72 | + Class<?> raw = Types.getRawType(type); |
| 73 | + if (response.body() == null) { |
| 74 | + return null; |
| 75 | + } |
| 76 | + if (byte[].class.isAssignableFrom(raw)) { |
| 77 | + return response.body().asInputStream().readAllBytes(); |
| 78 | + } |
| 79 | + if (InputStream.class.isAssignableFrom(raw)) { |
| 80 | + return response.body().asInputStream(); |
| 81 | + } |
| 82 | + return downloadToTempFile(response); |
| 83 | + } |
| 84 | + |
| 85 | + private File downloadToTempFile(Response response) throws IOException { |
| 86 | + String filename = extractFilename(response); |
| 87 | + File file; |
| 88 | + if (filename != null) { |
| 89 | + // Sanitize: strip path components to prevent path traversal |
| 90 | + String safeName = Paths.get(filename).getFileName().toString(); |
| 91 | + java.nio.file.Path tempDir = Files.createTempDirectory("feign-download"); |
| 92 | + file = Files.createFile(tempDir.resolve(safeName)).toFile(); |
| 93 | + tempDir.toFile().deleteOnExit(); |
45 | 94 | } else { |
46 | | - //The response is not encapsulated in the ApiResponse, decode the Dto as normal |
47 | | - return super.decode(response, type); |
| 95 | + file = Files.createTempFile("download-", "").toFile(); |
| 96 | + } |
| 97 | + file.deleteOnExit(); |
| 98 | + try (InputStream is = response.body().asInputStream()) { |
| 99 | + Files.copy(is, file.toPath(), StandardCopyOption.REPLACE_EXISTING); |
| 100 | + } |
| 101 | + return file; |
| 102 | + } |
| 103 | + |
| 104 | + private String extractFilename(Response response) { |
| 105 | + Collection<String> dispositions = response.headers().get("Content-Disposition"); |
| 106 | + if (dispositions == null) return null; |
| 107 | + for (String disposition : dispositions) { |
| 108 | + Matcher m = FILENAME_PATTERN.matcher(disposition); |
| 109 | + if (m.find()) { |
| 110 | + // Group 1: quoted filename (may contain spaces), Group 2: unquoted token |
| 111 | + return m.group(1) != null ? m.group(1) : m.group(2); |
| 112 | + } |
48 | 113 | } |
| 114 | + return null; |
49 | 115 | } |
50 | 116 | } |
0 commit comments