FINERACT-2380: create feign client

This commit is contained in:
Attila Budai
2025-10-17 14:25:39 +02:00
committed by Adam Saghy
parent f193bbbdf1
commit 553347b320
56 changed files with 7906 additions and 21 deletions
@@ -0,0 +1,84 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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 org.apache.fineract.client.adapter;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.module.SimpleModule;
import java.io.IOException;
import org.apache.fineract.client.models.ExternalId;
/**
* Custom Jackson adapter for ExternalId type serialization and deserialization. This adapter ensures that ExternalId
* objects are properly serialized to their string value and deserialized from string values.
*/
public final class ExternalIdAdapter {
private ExternalIdAdapter() {}
/**
* Jackson Serializer for ExternalId. Serializes an ExternalId object to its string value, or null if the ExternalId
* or its value is null.
*/
public static class Serializer extends JsonSerializer<ExternalId> {
@Override
public void serialize(ExternalId value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
if (value == null || value.getValue() == null) {
gen.writeNull();
} else {
gen.writeString(value.getValue());
}
}
}
/**
* Jackson Deserializer for ExternalId. Deserializes a string value to an ExternalId object, or null if the input is
* null.
*/
public static class Deserializer extends JsonDeserializer<ExternalId> {
@Override
public ExternalId deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
String value = p.getValueAsString();
if (value == null) {
return null;
}
ExternalId externalId = new ExternalId();
externalId.setValue(value);
return externalId;
}
}
/**
* Creates a Jackson SimpleModule configured with the ExternalId serializer and deserializer.
*
* @return A configured SimpleModule ready to be registered with an ObjectMapper
*/
public static SimpleModule createModule() {
SimpleModule module = new SimpleModule("ExternalIdModule");
module.addSerializer(ExternalId.class, new Serializer());
module.addDeserializer(ExternalId.class, new Deserializer());
return module;
}
}
@@ -0,0 +1,56 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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 org.apache.fineract.client.feign;
import feign.RequestInterceptor;
import feign.RequestTemplate;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
/**
* Request interceptor that adds Basic Authentication header to requests.
*/
public class BasicAuthRequestInterceptor implements RequestInterceptor {
private static final String AUTHORIZATION_HEADER = "Authorization";
private static final String BASIC_AUTH_PREFIX = "Basic ";
private final String credentials;
/**
* Creates a new BasicAuthRequestInterceptor with the specified credentials.
*
* @param username
* the username for authentication
* @param password
* the password for authentication
*/
public BasicAuthRequestInterceptor(String username, String password) {
if (username == null || password == null) {
throw new IllegalArgumentException("Username and password cannot be null");
}
String auth = username + ":" + password;
this.credentials = Base64.getEncoder().encodeToString(auth.getBytes(StandardCharsets.UTF_8));
}
@Override
public void apply(RequestTemplate template) {
template.header(AUTHORIZATION_HEADER, BASIC_AUTH_PREFIX + credentials);
}
}
@@ -0,0 +1,115 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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 org.apache.fineract.client.feign;
import feign.Request;
import java.nio.charset.Charset;
/**
* Base exception class for Feign client exceptions.
*/
public class FeignException extends RuntimeException {
private static final long serialVersionUID = 1L;
private final int status;
private final Request request;
private final byte[] responseBody;
private final String developerMessage;
private final String userMessage;
protected FeignException(int status, String message, Request request) {
this(status, message, request, (byte[]) null);
}
protected FeignException(int status, String message, Request request, Throwable cause) {
this(status, message, request, null, cause);
}
protected FeignException(int status, String message, Request request, byte[] responseBody) {
super(message);
this.status = status;
this.request = request;
this.responseBody = responseBody;
this.developerMessage = null;
this.userMessage = null;
}
protected FeignException(int status, String message, Request request, byte[] responseBody, Throwable cause) {
super(message, cause);
this.status = status;
this.request = request;
this.responseBody = responseBody;
this.developerMessage = null;
this.userMessage = null;
}
public FeignException(int status, String message, Request request, byte[] responseBody, String developerMessage, String userMessage) {
super(message);
this.status = status;
this.request = request;
this.responseBody = responseBody;
this.developerMessage = developerMessage;
this.userMessage = userMessage;
}
public int status() {
return status;
}
public Request request() {
return request;
}
public byte[] responseBody() {
return responseBody;
}
public String responseBodyAsString() {
return responseBody != null ? new String(responseBody, Charset.defaultCharset()) : null;
}
public String getDeveloperMessage() {
return developerMessage;
}
public String getUserMessage() {
return userMessage;
}
@Override
public String getMessage() {
StringBuilder sb = new StringBuilder();
sb.append("status ").append(status);
if (userMessage != null) {
sb.append(": ").append(userMessage);
}
if (developerMessage != null) {
sb.append(" (").append(developerMessage).append(")");
}
if (super.getMessage() != null && userMessage == null && developerMessage == null) {
sb.append(": ").append(super.getMessage());
}
return sb.toString();
}
}
@@ -0,0 +1,107 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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 org.apache.fineract.client.feign;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import feign.Response;
import feign.codec.ErrorDecoder;
import java.io.IOException;
import java.io.InputStream;
public class FineractErrorDecoder implements ErrorDecoder {
private final ErrorDecoder defaultDecoder = new Default();
private final ObjectMapper objectMapper = ObjectMapperFactory.getShared();
@Override
public Exception decode(String methodKey, Response response) {
try {
if (response.body() != null) {
byte[] bodyData = readResponseBody(response);
try {
JsonNode rootNode = objectMapper.readTree(bodyData);
String developerMessage = extractField(rootNode, "developerMessage");
String userMessage = extractField(rootNode, "userMessage");
String validationErrors = extractValidationErrors(rootNode);
if (developerMessage != null || userMessage != null || validationErrors != null) {
String enhancedDeveloperMessage = developerMessage;
if (validationErrors != null) {
enhancedDeveloperMessage = validationErrors;
}
return new FeignException(response.status(), userMessage != null ? userMessage : enhancedDeveloperMessage,
response.request(), bodyData, enhancedDeveloperMessage, userMessage);
}
} catch (IOException e) {
return defaultDecoder.decode(methodKey, response);
}
}
} catch (IOException e) {
return defaultDecoder.decode(methodKey, response);
}
return defaultDecoder.decode(methodKey, response);
}
private byte[] readResponseBody(Response response) throws IOException {
if (response.body() == null) {
return new byte[0];
}
try (InputStream inputStream = response.body().asInputStream()) {
return inputStream.readAllBytes();
}
}
private String extractField(JsonNode node, String fieldName) {
JsonNode fieldNode = node.get(fieldName);
return fieldNode != null && !fieldNode.isNull() ? fieldNode.asText() : null;
}
private String extractValidationErrors(JsonNode rootNode) {
JsonNode errorsNode = rootNode.get("errors");
if (errorsNode != null && errorsNode.isArray() && errorsNode.size() > 0) {
StringBuilder errors = new StringBuilder("Validation errors: ");
for (JsonNode error : errorsNode) {
String parameterName = extractField(error, "parameterName");
String defaultUserMessage = extractField(error, "defaultUserMessage");
String developerMessage = extractField(error, "developerMessage");
if (errors.length() > "Validation errors: ".length()) {
errors.append("; ");
}
if (parameterName != null) {
errors.append("[").append(parameterName).append("] ");
}
if (defaultUserMessage != null) {
errors.append(defaultUserMessage);
} else if (developerMessage != null) {
errors.append(developerMessage);
}
}
return errors.length() > "Validation errors: ".length() ? errors.toString() : null;
}
return null;
}
}
@@ -0,0 +1,293 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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 org.apache.fineract.client.feign;
import feign.Client;
import feign.Feign;
import feign.Request;
import feign.Retryer;
import feign.codec.Encoder;
import feign.hc5.ApacheHttp5Client;
import feign.jackson.JacksonDecoder;
import feign.jackson.JacksonEncoder;
import feign.slf4j.Slf4jLogger;
import java.security.cert.X509Certificate;
import java.util.concurrent.TimeUnit;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;
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.HttpClients;
import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManager;
import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManagerBuilder;
import org.apache.hc.client5.http.ssl.SSLConnectionSocketFactory;
import org.apache.hc.client5.http.ssl.SSLConnectionSocketFactoryBuilder;
import org.apache.hc.core5.util.TimeValue;
import org.apache.hc.core5.util.Timeout;
/**
* Configuration class for Feign client.
*/
public final class FineractFeignClientConfig {
public enum HttpClientType {
APACHE, OKHTTP
}
private final String baseUrl;
private final String username;
private final String password;
private final String tenantId;
private final int connectTimeout;
private final int readTimeout;
private final boolean debugEnabled;
private final long connectionTimeToLive;
private final TimeUnit connectionTimeToLiveUnit;
private final boolean disableSslVerification;
private final int maxConnTotal;
private final int maxConnPerRoute;
private final long idleConnectionEvictionTime;
private final TimeUnit idleConnectionEvictionTimeUnit;
private final HttpClientType clientType;
private volatile Client cachedHttpClient;
private FineractFeignClientConfig(Builder builder) {
this.baseUrl = builder.baseUrl;
this.username = builder.username;
this.password = builder.password;
this.tenantId = builder.tenantId;
this.connectTimeout = builder.connectTimeout;
this.readTimeout = builder.readTimeout;
this.debugEnabled = builder.debugEnabled;
this.connectionTimeToLive = builder.connectionTimeToLive;
this.connectionTimeToLiveUnit = builder.connectionTimeToLiveUnit;
this.disableSslVerification = builder.disableSslVerification;
this.maxConnTotal = builder.maxConnTotal;
this.maxConnPerRoute = builder.maxConnPerRoute;
this.idleConnectionEvictionTime = builder.idleConnectionEvictionTime;
this.idleConnectionEvictionTimeUnit = builder.idleConnectionEvictionTimeUnit;
this.clientType = builder.clientType;
}
public static Builder builder() {
return new Builder();
}
public <T> T createClient(Class<T> apiType) {
JacksonEncoder jacksonEncoder = new JacksonEncoder(ObjectMapperFactory.getShared());
Encoder multipartEncoder = new FineractMultipartEncoder(jacksonEncoder);
return Feign.builder().client(getOrCreateHttpClient()).encoder(multipartEncoder)
.decoder(new JacksonDecoder(ObjectMapperFactory.getShared())).errorDecoder(new FineractErrorDecoder())
.options(new Request.Options(connectTimeout, TimeUnit.MILLISECONDS, readTimeout, TimeUnit.MILLISECONDS, true))
.retryer(Retryer.NEVER_RETRY).requestInterceptor(new BasicAuthRequestInterceptor(username, password))
.requestInterceptor(new TenantIdRequestInterceptor(tenantId)).logger(new Slf4jLogger(apiType))
.logLevel(debugEnabled ? feign.Logger.Level.FULL : feign.Logger.Level.BASIC).target(apiType, baseUrl);
}
private Client getOrCreateHttpClient() {
if (cachedHttpClient == null) {
synchronized (this) {
if (cachedHttpClient == null) {
cachedHttpClient = createHttpClient();
}
}
}
return cachedHttpClient;
}
private Client createHttpClient() {
switch (clientType) {
case APACHE:
return createApacheHttpClient();
case OKHTTP:
return createOkHttpClient();
default:
throw new IllegalStateException("Unsupported HTTP client type: " + clientType);
}
}
private Client createApacheHttpClient() {
try {
PoolingHttpClientConnectionManagerBuilder connManagerBuilder = PoolingHttpClientConnectionManagerBuilder.create()
.setMaxConnTotal(maxConnTotal).setMaxConnPerRoute(maxConnPerRoute);
if (disableSslVerification) {
SSLContext sslContext = createTrustAllSslContext();
SSLConnectionSocketFactory sslSocketFactory = SSLConnectionSocketFactoryBuilder.create().setSslContext(sslContext).build();
connManagerBuilder.setSSLSocketFactory(sslSocketFactory);
}
if (connectionTimeToLive > 0) {
connManagerBuilder.setConnectionTimeToLive(TimeValue.of(connectionTimeToLive, connectionTimeToLiveUnit));
}
PoolingHttpClientConnectionManager connectionManager = connManagerBuilder.build();
CloseableHttpClient httpClient = HttpClients.custom().setConnectionManager(connectionManager)
.setDefaultRequestConfig(RequestConfig.custom().setConnectTimeout(Timeout.ofMilliseconds(connectTimeout))
.setResponseTimeout(Timeout.ofMilliseconds(readTimeout)).build())
.evictIdleConnections(TimeValue.of(idleConnectionEvictionTime, idleConnectionEvictionTimeUnit))
.evictExpiredConnections().build();
return new ApacheHttp5Client(httpClient);
} catch (Exception e) {
throw new RuntimeException("Failed to create Apache HTTP client", e);
}
}
private Client createOkHttpClient() {
try {
okhttp3.OkHttpClient.Builder builder = new okhttp3.OkHttpClient.Builder().connectTimeout(connectTimeout, TimeUnit.MILLISECONDS)
.readTimeout(readTimeout, TimeUnit.MILLISECONDS)
.connectionPool(new okhttp3.ConnectionPool(maxConnTotal, connectionTimeToLive > 0 ? connectionTimeToLive : 5,
connectionTimeToLive > 0 ? connectionTimeToLiveUnit : TimeUnit.MINUTES));
if (disableSslVerification) {
SSLContext sslContext = createTrustAllSslContext();
builder.sslSocketFactory(sslContext.getSocketFactory(), createTrustAllManager());
builder.hostnameVerifier((hostname, session) -> true);
}
return new feign.okhttp.OkHttpClient(builder.build());
} catch (Exception e) {
throw new RuntimeException("Failed to create OkHttp client", e);
}
}
private X509TrustManager createTrustAllManager() {
return new X509TrustManager() {
@Override
public X509Certificate[] getAcceptedIssuers() {
return new X509Certificate[0];
}
@Override
public void checkClientTrusted(X509Certificate[] certs, String authType) {}
@Override
public void checkServerTrusted(X509Certificate[] certs, String authType) {}
};
}
private SSLContext createTrustAllSslContext() throws Exception {
TrustManager[] trustAllCerts = new TrustManager[] { createTrustAllManager() };
SSLContext sslContext = SSLContext.getInstance("TLS");
sslContext.init(null, trustAllCerts, new java.security.SecureRandom());
return sslContext;
}
public static class Builder {
private String baseUrl;
private String username;
private String password;
private String tenantId = "default";
private int connectTimeout = 30000; // 30 seconds
private int readTimeout = 60000; // 60 seconds
private boolean debugEnabled = false;
private long connectionTimeToLive = -1;
private TimeUnit connectionTimeToLiveUnit = TimeUnit.MILLISECONDS;
private boolean disableSslVerification = false;
private int maxConnTotal = 200;
private int maxConnPerRoute = 20;
private long idleConnectionEvictionTime = 30;
private TimeUnit idleConnectionEvictionTimeUnit = TimeUnit.SECONDS;
private HttpClientType clientType = HttpClientType.APACHE;
public Builder baseUrl(String baseUrl) {
this.baseUrl = baseUrl;
return this;
}
public Builder credentials(String username, String password) {
this.username = username;
this.password = password;
return this;
}
public Builder tenantId(String tenantId) {
this.tenantId = tenantId;
return this;
}
public Builder connectTimeout(int timeout, TimeUnit unit) {
this.connectTimeout = (int) unit.toMillis(timeout);
return this;
}
public Builder readTimeout(int timeout, TimeUnit unit) {
this.readTimeout = (int) unit.toMillis(timeout);
return this;
}
public Builder connectionTimeToLive(long ttl, TimeUnit unit) {
this.connectionTimeToLive = ttl;
this.connectionTimeToLiveUnit = unit;
return this;
}
public Builder debugEnabled(boolean debugEnabled) {
this.debugEnabled = debugEnabled;
return this;
}
public Builder disableSslVerification(boolean disableSslVerification) {
this.disableSslVerification = disableSslVerification;
return this;
}
public Builder maxConnTotal(int maxConnTotal) {
this.maxConnTotal = maxConnTotal;
return this;
}
public Builder maxConnPerRoute(int maxConnPerRoute) {
this.maxConnPerRoute = maxConnPerRoute;
return this;
}
public Builder idleConnectionEvictionTime(long time, TimeUnit unit) {
this.idleConnectionEvictionTime = time;
this.idleConnectionEvictionTimeUnit = unit;
return this;
}
public Builder httpClientType(HttpClientType clientType) {
this.clientType = clientType;
return this;
}
public FineractFeignClientConfig build() {
if (baseUrl == null || baseUrl.trim().isEmpty()) {
throw new IllegalStateException("baseUrl is required");
}
if (username == null || username.trim().isEmpty()) {
throw new IllegalStateException("username is required");
}
if (password == null) {
throw new IllegalStateException("password is required");
}
return new FineractFeignClientConfig(this);
}
}
}
@@ -0,0 +1,198 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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 org.apache.fineract.client.feign;
import feign.RequestTemplate;
import feign.codec.EncodeException;
import feign.codec.Encoder;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.Type;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import org.apache.hc.client5.http.entity.mime.MultipartEntityBuilder;
import org.apache.hc.core5.http.HttpEntity;
/**
* Custom multipart encoder for Fineract Feign clients. Uses Apache HttpClient's MultipartEntityBuilder to properly
* construct multipart/form-data requests.
*/
public class FineractMultipartEncoder implements Encoder {
private final Encoder delegate;
public FineractMultipartEncoder(Encoder delegate) {
this.delegate = delegate;
}
@Override
public void encode(Object object, Type bodyType, RequestTemplate template) throws EncodeException {
if (object instanceof MultipartData) {
encodeMultipart((MultipartData) object, template);
} else if (object instanceof File) {
encodeFileAsMultipart((File) object, template);
} else if (object instanceof String && template.headers().containsKey("Content-Type")) {
String contentType = template.headers().get("Content-Type").iterator().next();
if (contentType.startsWith("text/html") || contentType.startsWith("text/plain")) {
byte[] bodyBytes = ((String) object).getBytes(StandardCharsets.UTF_8);
template.body(bodyBytes, StandardCharsets.UTF_8);
} else {
delegate.encode(object, bodyType, template);
}
} else {
delegate.encode(object, bodyType, template);
}
}
private void encodeFileAsMultipart(File file, RequestTemplate template) throws EncodeException {
try {
byte[] fileData = Files.readAllBytes(file.toPath());
String contentType = detectContentType(file);
MultipartData multipartData = new MultipartData().addFile("file", file.getName(), fileData, contentType);
encodeMultipart(multipartData, template);
} catch (IOException e) {
throw new EncodeException("Failed to encode File as multipart: " + file, e);
}
}
private String detectContentType(File file) {
try {
String contentType = Files.probeContentType(file.toPath());
return contentType != null ? contentType : "application/octet-stream";
} catch (IOException e) {
return "application/octet-stream";
}
}
private void encodeMultipart(MultipartData data, RequestTemplate template) throws EncodeException {
try {
MultipartEntityBuilder builder = MultipartEntityBuilder.create();
builder.setMode(org.apache.hc.client5.http.entity.mime.HttpMultipartMode.STRICT);
for (MultipartData.Part part : data.getParts()) {
if (part.getFileData() != null) {
org.apache.hc.core5.http.ContentType ct = org.apache.hc.core5.http.ContentType.create(part.getContentType());
builder.addBinaryBody(part.getName(), part.getFileData(), ct, part.getFileName());
} else if (part.getTextValue() != null) {
builder.addTextBody(part.getName(), part.getTextValue(), org.apache.hc.core5.http.ContentType.TEXT_PLAIN);
}
}
HttpEntity entity = builder.build();
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
entity.writeTo(outputStream);
byte[] body = outputStream.toByteArray();
template.body(body, null);
template.removeHeader("Content-Type");
String contentTypeValue = entity.getContentType();
String cleanContentType = contentTypeValue;
if (contentTypeValue.contains(";")) {
int firstSemicolon = contentTypeValue.indexOf(';');
String mainType = contentTypeValue.substring(0, firstSemicolon).trim();
String paramsSection = contentTypeValue.substring(firstSemicolon + 1);
String boundary = null;
int boundaryIndex = paramsSection.indexOf("boundary=");
if (boundaryIndex != -1) {
int boundaryStart = boundaryIndex;
int boundaryEnd = paramsSection.indexOf(';', boundaryStart);
if (boundaryEnd == -1) {
boundary = paramsSection.substring(boundaryStart).trim();
} else {
boundary = paramsSection.substring(boundaryStart, boundaryEnd).trim();
}
}
if (boundary != null) {
cleanContentType = mainType + "; " + boundary;
}
}
template.header("Content-Type", cleanContentType);
} catch (IOException e) {
throw new EncodeException("Failed to encode multipart request", e);
}
}
public static class MultipartData {
private final java.util.List<Part> parts = new java.util.ArrayList<>();
public MultipartData addFile(String name, String fileName, byte[] data, String contentType) {
parts.add(new Part(name, fileName, data, contentType));
return this;
}
public MultipartData addText(String name, String value) {
parts.add(new Part(name, value));
return this;
}
public java.util.List<Part> getParts() {
return parts;
}
public static class Part {
private final String name;
private final String fileName;
private final byte[] fileData;
private final String contentType;
private final String textValue;
public Part(String name, String fileName, byte[] fileData, String contentType) {
this.name = name;
this.fileName = fileName;
this.fileData = fileData;
this.contentType = contentType;
this.textValue = null;
}
public Part(String name, String textValue) {
this.name = name;
this.textValue = textValue;
this.fileName = null;
this.fileData = null;
this.contentType = null;
}
public String getName() {
return name;
}
public String getFileName() {
return fileName;
}
public byte[] getFileData() {
return fileData;
}
public String getContentType() {
return contentType;
}
public String getTextValue() {
return textValue;
}
}
}
}
@@ -0,0 +1,73 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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 org.apache.fineract.client.feign;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import org.apache.fineract.client.adapter.ExternalIdAdapter;
/**
* Factory for creating and configuring Jackson ObjectMapper instances.
*/
public final class ObjectMapperFactory {
private static final ObjectMapper INSTANCE = createObjectMapper();
private ObjectMapperFactory() {
// Private constructor to prevent instantiation
}
/**
* Creates and configures a new ObjectMapper instance.
*
* @return A new configured ObjectMapper instance
*/
public static ObjectMapper createObjectMapper() {
ObjectMapper mapper = new ObjectMapper();
// Configure the ObjectMapper
mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
mapper.configure(DeserializationFeature.READ_UNKNOWN_ENUM_VALUES_AS_NULL, true);
mapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false);
// Register Java 8 date/time support
mapper.registerModule(new JavaTimeModule());
// Register ExternalId adapter
mapper.registerModule(ExternalIdAdapter.createModule());
// Disable FAIL_ON_EMPTY_BEANS for empty responses
mapper.disable(SerializationFeature.FAIL_ON_EMPTY_BEANS);
return mapper;
}
/**
* Returns a shared, pre-configured ObjectMapper instance.
*
* @return A shared ObjectMapper instance
*/
public static ObjectMapper getShared() {
return INSTANCE;
}
}
@@ -0,0 +1,42 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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 org.apache.fineract.client.feign;
import feign.RequestInterceptor;
import feign.RequestTemplate;
public class TenantIdRequestInterceptor implements RequestInterceptor {
private final String tenantId;
public TenantIdRequestInterceptor(String tenantId) {
this.tenantId = tenantId;
}
@Override
public void apply(RequestTemplate template) {
template.header("Fineract-Platform-TenantId", tenantId);
if (!template.headers().containsKey("Content-Type")) {
template.header("Content-Type", "application/json");
}
if (!template.headers().containsKey("Accept")) {
template.header("Accept", "application/json");
}
}
}
@@ -0,0 +1,140 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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 org.apache.fineract.client.feign.services;
import feign.Param;
import feign.RequestLine;
import feign.Response;
import java.util.List;
import org.apache.fineract.client.models.DeleteEntityTypeEntityIdDocumentsResponse;
import org.apache.fineract.client.models.DocumentData;
import org.apache.fineract.client.models.PostEntityTypeEntityIdDocumentsResponse;
import org.apache.fineract.client.models.PutEntityTypeEntityIdDocumentsResponse;
/**
* This class was originally generated by OpenAPI Generator (https://openapi-generator.tech), but then had to be
* manually edited to manually fix https://issues.apache.org/jira/browse/FINERACT-1227. If we could fix our OpenAPI /
* Swagger YAML generation from the JAX RS and OpenAPI annotation to have the correct notation for binary document files
* and images, then this could be removed again.
*/
public interface DocumentsApiFixed {
/**
* Create a Document Note: A document is created using a Multi-part form upload Body Parts name : Name or summary of
* the document description : Description of the document file : The file to be uploaded Mandatory Fields : file and
* description
*
* @param entityType
* entityType (required)
* @param entityId
* entityId (required)
* @param file
* (required)
* @param name
* name (optional)
* @param description
* description (optional)
* @return PostEntityTypeEntityIdDocumentsResponse
*/
@RequestLine("POST /v1/{entityType}/{entityId}/documents")
PostEntityTypeEntityIdDocumentsResponse createDocument(@Param("entityType") String entityType, @Param("entityId") Long entityId,
org.apache.fineract.client.feign.FineractMultipartEncoder.MultipartData multipartData);
/**
* Remove a Document
*
* @param entityType
* entityType (required)
* @param entityId
* entityId (required)
* @param documentId
* documentId (required)
* @return DeleteEntityTypeEntityIdDocumentsResponse
*/
@RequestLine("DELETE /v1/{entityType}/{entityId}/documents/{documentId}")
DeleteEntityTypeEntityIdDocumentsResponse deleteDocument(@Param("entityType") String entityType, @Param("entityId") Long entityId,
@Param("documentId") Long documentId);
/**
* Retrieve Binary File associated with Document Request used to download the file associated with the document
* Example Requests: clients/1/documents/1/attachment loans/1/documents/1/attachment
*
* @param entityType
* entityType (required)
* @param entityId
* entityId (required)
* @param documentId
* documentId (required)
* @return Response
*/
@RequestLine("GET /v1/{entityType}/{entityId}/documents/{documentId}/attachment")
@feign.Headers("Accept: */*")
Response downloadFile(@Param("entityType") String entityType, @Param("entityId") Long entityId, @Param("documentId") Long documentId);
/**
* Retrieve a Document Example Requests: clients/1/documents/1 loans/1/documents/1
* client_identifiers/1/documents/1?fields=name,description
*
* @param entityType
* entityType (required)
* @param entityId
* entityId (required)
* @param documentId
* documentId (required)
* @return DocumentData
*/
@RequestLine("GET /v1/{entityType}/{entityId}/documents/{documentId}")
DocumentData getDocument(@Param("entityType") String entityType, @Param("entityId") Long entityId,
@Param("documentId") Long documentId);
/**
* List documents Example Requests: clients/1/documents client_identifiers/1/documents
* loans/1/documents?fields=name,description
*
* @param entityType
* entityType (required)
* @param entityId
* entityId (required)
* @return List&lt;DocumentData&gt;
*/
@RequestLine("GET /v1/{entityType}/{entityId}/documents")
List<DocumentData> retrieveAllDocuments(@Param("entityType") String entityType, @Param("entityId") Long entityId);
/**
* Update a Document Note: A document is updated using a Multi-part form upload Body Parts name Name or summary of
* the document description Description of the document file The file to be uploaded
*
* @param entityType
* entityType (required)
* @param entityId
* entityId (required)
* @param documentId
* documentId (required)
* @param file
* (optional)
* @param name
* name (optional)
* @param description
* description (optional)
* @return PutEntityTypeEntityIdDocumentsResponse
*/
@RequestLine("PUT /v1/{entityType}/{entityId}/documents/{documentId}")
PutEntityTypeEntityIdDocumentsResponse updateDocument(@Param("entityType") String entityType, @Param("entityId") Long entityId,
@Param("documentId") Long documentId, org.apache.fineract.client.feign.FineractMultipartEncoder.MultipartData multipartData);
}
@@ -0,0 +1,97 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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 org.apache.fineract.client.feign.services;
import feign.Headers;
import feign.Param;
import feign.QueryMap;
import feign.RequestLine;
import feign.Response;
import java.io.File;
import java.util.Map;
/**
* Client API (Feign) for /images.
*
* This class is entirely hand-written, inspired by DocumentsApiFixed, and from /images methods which currently end up
* in DefaultApi (see <a href="https://issues.apache.org/jira/browse/FINERACT-1222">FINERACT-1222</a>), but fixed for
* bugs in the code generation (see <a href="https://issues.apache.org/jira/browse/FINERACT-1227">FINERACT-1227</a>).
*
* Note: For image uploads, use {@link #prepareFileUpload(File)} to prepare file data as a Data URL.
*/
public interface ImagesApi {
@RequestLine("POST /v1/{entityType}/{entityId}/images")
@Headers("Content-Type: text/html")
Response create(@Param("entityType") String entityType, @Param("entityId") Long entityId, String dataUrl);
@RequestLine("GET /v1/{entityType}/{entityId}/images")
Response get(@Param("entityType") String entityType, @Param("entityId") Long entityId, @QueryMap Map<String, Object> queryParams);
@RequestLine("PUT /v1/{entityType}/{entityId}/images")
@Headers("Content-Type: text/html")
Response update(@Param("entityType") String entityType, @Param("entityId") Long entityId, String dataUrl);
@RequestLine("DELETE /v1/{entityType}/{entityId}/images")
Response delete(@Param("entityType") String entityType, @Param("entityId") Long entityId);
static String prepareFileUpload(File file) {
try {
byte[] fileData = java.nio.file.Files.readAllBytes(file.toPath());
String contentType = detectMediaType(file.getName());
String dataUrlPrefix = getDataUrlPrefix(contentType);
String base64Data = java.util.Base64.getEncoder().encodeToString(fileData);
return dataUrlPrefix + base64Data;
} catch (java.io.IOException e) {
throw new RuntimeException("Failed to prepare file for upload: " + file, e);
}
}
private static String getDataUrlPrefix(String contentType) {
return "data:" + contentType + ";base64,";
}
private static String detectMediaType(String fileName) {
if (fileName == null) {
return "application/octet-stream";
}
int dotPos = fileName.lastIndexOf('.');
if (dotPos == -1) {
return "application/octet-stream";
}
String ext = fileName.substring(dotPos + 1).toLowerCase();
switch (ext) {
case "jpg":
case "jpeg":
return "image/jpeg";
case "png":
return "image/png";
case "gif":
return "image/gif";
case "tif":
case "tiff":
return "image/tiff";
case "pdf":
return "application/pdf";
default:
return "application/octet-stream";
}
}
}
@@ -0,0 +1,66 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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 org.apache.fineract.client.feign.services;
import feign.Param;
import feign.QueryMap;
import feign.RequestLine;
import feign.Response;
import java.util.Map;
import org.apache.fineract.client.models.RunReportsResponse;
public interface RunReportsApi {
/**
* Running a Report This resource allows you to run and receive output from pre-defined Apache Fineract reports.
* Reports can also be used to provide data for searching and workflow functionality. The default output is a JSON
* formatted "Generic Resultset". The Generic Resultset contains Column Heading as well as Data information.
* However, you can export to CSV format by simply adding "&amp;exportCSV=true" to the end of your URL. If Pentaho
* reports have been pre-defined, they can also be run through this resource. Pentaho reports can return HTML, PDF
* or CSV formats. The Apache Fineract reference application uses a JQuery plugin called stretchy reporting which,
* itself, uses this reports resource to provide a pretty flexible reporting User Interface (UI). Example Requests:
* runreports/Client%20Listing?R_officeId=1 runreports/Client%20Listing?R_officeId=1&amp;exportCSV=true
* runreports/OfficeIdSelectOne?R_officeId=1&amp;parameterType=true
* runreports/OfficeIdSelectOne?R_officeId=1&amp;parameterType=true&amp;exportCSV=true
* runreports/Expected%20Payments%20By%20Date%20-%20Formatted?R_endDate=2013-04-30&amp;R_loanOfficerId=-1&amp;R_officeId=1&amp;R_startDate=2013-04-16&amp;output-type=HTML&amp;R_officeId=1
* runreports/Expected%20Payments%20By%20Date%20-%20Formatted?R_endDate=2013-04-30&amp;R_loanOfficerId=-1&amp;R_officeId=1&amp;R_startDate=2013-04-16&amp;output-type=XLS&amp;R_officeId=1
* runreports/Expected%20Payments%20By%20Date%20-%20Formatted?R_endDate=2013-04-30&amp;R_loanOfficerId=-1&amp;R_officeId=1&amp;R_startDate=2013-04-16&amp;output-type=CSV&amp;R_officeId=1
* runreports/Expected%20Payments%20By%20Date%20-%20Formatted?R_endDate=2013-04-30&amp;R_loanOfficerId=-1&amp;R_officeId=1&amp;R_startDate=2013-04-16&amp;output-type=PDF&amp;R_officeId=1
*
* @param reportName
* reportName (required)
* @param parameters
* Dynamic query parameters for the report (required)
* @return RunReportsResponse
*/
@RequestLine("GET /v1/runreports/{reportName}")
RunReportsResponse runReportGetData(@Param("reportName") String reportName, @QueryMap Map<String, String> parameters);
/**
* Run Report which returns a response such as a PDF, CSV, XLS or XLSX file.
*
* @param reportName
* reportName (required)
* @param parameters
* Dynamic query parameters for the report (required)
* @return Response containing the file content
*/
@RequestLine("GET /v1/runreports/{reportName}")
Response runReportGetFile(@Param("reportName") String reportName, @QueryMap Map<String, String> parameters);
}
@@ -0,0 +1,91 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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 org.apache.fineract.client.feign.util;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import feign.FeignException;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
/**
* Exception thrown by {@link FeignCalls} utility when Feign calls fail.
*/
@Slf4j
@Getter
public class CallFailedRuntimeException extends RuntimeException {
private final int status;
private final String developerMessage;
public CallFailedRuntimeException(FeignException cause) {
super(createMessage(cause), cause);
this.status = cause.status();
this.developerMessage = extractDeveloperMessage(cause);
}
private static String createMessage(FeignException e) {
StringBuilder sb = new StringBuilder("HTTP failed: status=").append(e.status());
if (e.request() != null) {
sb.append(", request=").append(e.request().url());
}
String contentString = e.contentUTF8();
if (contentString != null && !contentString.isEmpty()) {
sb.append(", errorBody=").append(contentString);
}
return sb.toString();
}
private static String extractDeveloperMessage(FeignException e) {
try {
byte[] content = e.content();
if (content == null || content.length == 0) {
return e.getMessage();
}
String contentString = new String(content, StandardCharsets.UTF_8);
ObjectMapper mapper = new ObjectMapper();
JsonNode root = mapper.readTree(contentString);
if (root.has("developerMessage")) {
return root.get("developerMessage").asText();
}
if (root.has("errors")) {
JsonNode errors = root.get("errors");
if (errors.isArray() && errors.size() > 0) {
JsonNode firstError = errors.get(0);
if (firstError.has("developerMessage")) {
return firstError.get("developerMessage").asText();
}
}
}
return contentString;
} catch (IOException ex) {
log.warn("Failed to extract developer message from error response", ex);
return e.getMessage();
}
}
}
@@ -0,0 +1,80 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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 org.apache.fineract.client.feign.util;
import feign.FeignException;
import java.util.function.Supplier;
/**
* Extension methods for Feign calls. This class is recommended to be statically imported.
*/
public final class FeignCalls {
private FeignCalls() {}
/**
* Execute a Feign call, expecting success, returning strongly typed body. This covers the most typical use and is
* thus most used.
*
* @param feignCall
* the Feign call to execute
* @return the result of the successful call (never null)
* @throws CallFailedRuntimeException
* thrown if a problem occurred talking to the server, or the HTTP response code was not [200..300)
* successful
*/
public static <T> T ok(Supplier<T> feignCall) throws CallFailedRuntimeException {
try {
return feignCall.get();
} catch (FeignException e) {
throw new CallFailedRuntimeException(e);
}
}
/**
* Execute a Feign call that returns void. This is useful for operations that don't return a response body.
*
* @param feignCall
* the Feign call to execute
* @throws CallFailedRuntimeException
* thrown if a problem occurred talking to the server, or the HTTP response code was not [200..300)
* successful
*/
public static void executeVoid(Runnable feignCall) throws CallFailedRuntimeException {
try {
feignCall.run();
} catch (FeignException e) {
throw new CallFailedRuntimeException(e);
}
}
/**
* Execute a Feign call, returning the result without throwing CallFailedRuntimeException. This is useful when you
* want to handle FeignException yourself.
*
* @param feignCall
* the Feign call to execute
* @return the result of the call
* @throws FeignException
* thrown if a problem occurred talking to the server
*/
public static <T> T execute(Supplier<T> feignCall) throws FeignException {
return feignCall.get();
}
}
@@ -0,0 +1,136 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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 org.apache.fineract.client.util;
import feign.Response;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.util.Optional;
import org.apache.fineract.client.feign.services.DocumentsApiFixed;
import org.apache.fineract.client.feign.services.ImagesApi;
/**
* Convenience utilities for file handling in Feign API calls.
*
* Provides helper methods for: - Media type detection from file extensions - File name extraction from HTTP response
* headers - Content type probing
*
* Used in conjunction with {@link DocumentsApiFixed} and {@link ImagesApi} which use Data URL format for file uploads
* (base64-encoded strings with data URI scheme).
*/
public final class FeignParts {
private FeignParts() {}
/**
* Determine the media type based on file extension.
*
* @param fileName
* the name of the file
* @return the media type string, or null if not recognized
*/
public static String mediaType(String fileName) {
if (fileName == null) {
return null;
}
int dotPos = fileName.lastIndexOf('.');
if (dotPos == -1) {
return null;
}
String ext = fileName.substring(dotPos + 1).toLowerCase();
switch (ext) {
case "jpg":
case "jpeg":
return "image/jpeg";
case "png":
return "image/png";
case "tif":
case "tiff":
return "image/tiff";
case "gif":
return "image/gif";
case "pdf":
return "application/pdf";
case "docx":
return "application/vnd.openxmlformats-officedocument.wordprocessingml.document";
case "doc":
return "application/msword";
case "xlsx":
return "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet";
case "xls":
return "application/vnd.ms-excel";
case "odt":
return "application/vnd.oasis.opendocument.text";
case "ods":
return "application/vnd.oasis.opendocument.spreadsheet";
case "txt":
return "text/plain";
default:
return "application/octet-stream";
}
}
/**
* Extract the file name from the Content-Disposition header of a response.
*
* @param response
* the HTTP response
* @return Optional containing the file name if present
*/
public static Optional<String> fileName(Response response) {
if (response.headers() == null) {
return Optional.empty();
}
java.util.Collection<String> contentDispositionHeaders = response.headers().get("Content-Disposition");
if (contentDispositionHeaders == null || contentDispositionHeaders.isEmpty()) {
return Optional.empty();
}
String contentDisposition = contentDispositionHeaders.iterator().next();
if (contentDisposition == null) {
return Optional.empty();
}
int i = contentDisposition.indexOf("; filename=\"");
if (i == -1) {
return Optional.empty();
}
return Optional.of(contentDisposition.substring(i + "; filename=\"".length(), contentDisposition.length() - 1));
}
/**
* Probe the content type of a file using Files.probeContentType.
*
* @param file
* the file to probe
* @return the content type, or application/octet-stream as fallback
* @throws IOException
* if an I/O error occurs
*/
public static String probeContentType(File file) throws IOException {
String contentType = Files.probeContentType(file.toPath());
if (contentType == null) {
contentType = mediaType(file.getName());
}
return contentType != null ? contentType : "application/octet-stream";
}
}
@@ -0,0 +1,47 @@
package {{package}};
import feign.*;
import feign.Param;
import feign.RequestLine;
{{#imports}}import {{import}};
{{/imports}}
import java.util.List;
import java.util.Map;
public interface {{classname}} {
{{#operations}}
{{#operation}}
/**
* {{summary}}
* {{notes}}
{{#allParams}}
* @param {{paramName}} {{description}}
{{/allParams}}
* @return {{#returnType}}{{{returnType}}}{{/returnType}}{{^returnType}}void{{/returnType}}
*/
@RequestLine("{{httpMethod}} {{path}}")
{{#returnType}}{{{returnType}}}{{/returnType}}{{^returnType}}void{{/returnType}} {{operationId}}(
{{#pathParams}}
@Param("{{baseName}}") {{{dataType}}} {{paramName}}{{^-last}},{{/-last}}
{{/pathParams}}
{{#bodyParam}}
{{#hasPathParams}},{{/hasPathParams}}{{{dataType}}} {{paramName}}
{{/bodyParam}}
{{#hasQueryParams}}
{{#hasPathParams}}{{^bodyParam}},{{/bodyParam}}{{/hasPathParams}}{{#bodyParam}},{{/bodyParam}}
@QueryMap Map<String, Object> queryParams
{{/hasQueryParams}}
{{#hasHeaderParams}}
{{#hasPathParams}}{{^bodyParam}}{{^hasQueryParams}},{{/hasQueryParams}}{{/bodyParam}}{{/hasPathParams}}
{{#bodyParam}}{{^hasQueryParams}},{{/hasQueryParams}}{{/bodyParam}}
{{#hasQueryParams}},{{/hasQueryParams}}
@HeaderMap Map<String, Object> headerParams
{{/hasHeaderParams}}
);
{{/operation}}
{{/operations}}
}