FINERACT-2380: create feign client
This commit is contained in:
+84
@@ -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;
|
||||
}
|
||||
}
|
||||
+56
@@ -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);
|
||||
}
|
||||
}
|
||||
+115
@@ -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();
|
||||
}
|
||||
}
|
||||
+107
@@ -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;
|
||||
}
|
||||
}
|
||||
+1002
File diff suppressed because it is too large
Load Diff
+293
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
+198
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+73
@@ -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;
|
||||
}
|
||||
}
|
||||
+42
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
+140
@@ -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<DocumentData>
|
||||
*/
|
||||
@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);
|
||||
}
|
||||
+97
@@ -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";
|
||||
}
|
||||
}
|
||||
}
|
||||
+66
@@ -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 "&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&exportCSV=true
|
||||
* runreports/OfficeIdSelectOne?R_officeId=1&parameterType=true
|
||||
* runreports/OfficeIdSelectOne?R_officeId=1&parameterType=true&exportCSV=true
|
||||
* runreports/Expected%20Payments%20By%20Date%20-%20Formatted?R_endDate=2013-04-30&R_loanOfficerId=-1&R_officeId=1&R_startDate=2013-04-16&output-type=HTML&R_officeId=1
|
||||
* runreports/Expected%20Payments%20By%20Date%20-%20Formatted?R_endDate=2013-04-30&R_loanOfficerId=-1&R_officeId=1&R_startDate=2013-04-16&output-type=XLS&R_officeId=1
|
||||
* runreports/Expected%20Payments%20By%20Date%20-%20Formatted?R_endDate=2013-04-30&R_loanOfficerId=-1&R_officeId=1&R_startDate=2013-04-16&output-type=CSV&R_officeId=1
|
||||
* runreports/Expected%20Payments%20By%20Date%20-%20Formatted?R_endDate=2013-04-30&R_loanOfficerId=-1&R_officeId=1&R_startDate=2013-04-16&output-type=PDF&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);
|
||||
}
|
||||
+91
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
+80
@@ -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}}
|
||||
}
|
||||
Reference in New Issue
Block a user