FINERACT-2380: create feign client
This commit is contained in:
@@ -0,0 +1,32 @@
|
||||
**/README.md
|
||||
**/pom.xml
|
||||
**/build.sbt
|
||||
**/*.gradle
|
||||
**/.gitignore
|
||||
**/git_push.sh
|
||||
**/api/*
|
||||
**/gradle*
|
||||
**/gradle
|
||||
**/src/main/AndroidManifest.xml
|
||||
|
||||
# https://issues.apache.org/jira/browse/FINERACT-1231
|
||||
**/feign/*.java
|
||||
!**/feign/CollectionFormats.java
|
||||
!**/feign/StringUtil.java
|
||||
|
||||
# Manual API overrides - do not regenerate
|
||||
# https://issues.apache.org/jira/browse/FINERACT-1263
|
||||
**/services/RunReportsApi.java
|
||||
**/services/ImagesApi.java
|
||||
**/services/DocumentsApiFixed.java
|
||||
|
||||
# Utility classes - do not regenerate
|
||||
**/util/*.java
|
||||
**/adapter/*.java
|
||||
|
||||
# Feign configuration - do not regenerate
|
||||
**/feign/FineractFeignClient.java
|
||||
**/feign/FineractFeignClientConfig.java
|
||||
**/feign/BasicAuthRequestInterceptor.java
|
||||
**/feign/ObjectMapperFactory.java
|
||||
**/feign/FeignException.java
|
||||
@@ -0,0 +1,194 @@
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
apply plugin: 'org.openapi.generator'
|
||||
apply plugin: 'jacoco'
|
||||
description = 'Fineract Client with Feign'
|
||||
|
||||
apply from: 'dependencies.gradle'
|
||||
|
||||
openApiMeta {
|
||||
generatorName = 'Fineract-Feign'
|
||||
packageName = 'org.apache.fineract.client.feign'
|
||||
outputFolder = "$buildDir/meta".toString()
|
||||
}
|
||||
|
||||
openApiValidate {
|
||||
inputSpec = "file:///$swaggerFile"
|
||||
recommend = true
|
||||
}
|
||||
|
||||
tasks.register('buildJavaSdk', org.openapitools.generator.gradle.plugin.tasks.GenerateTask) {
|
||||
generatorName = 'java'
|
||||
library = 'feign'
|
||||
verbose = false
|
||||
validateSpec = false
|
||||
skipValidateSpec = true
|
||||
inputSpec = "file:///$swaggerFile"
|
||||
outputDir = "$buildDir/generated/temp-java".toString()
|
||||
templateDir = "$projectDir/src/main/resources/templates/java"
|
||||
groupId = 'org.apache.fineract'
|
||||
apiPackage = 'org.apache.fineract.client.feign.services'
|
||||
invokerPackage = 'org.apache.fineract.client.feign'
|
||||
modelPackage = 'org.apache.fineract.client.models'
|
||||
generateModelTests = false
|
||||
generateApiTests = false
|
||||
ignoreFileOverride = "$projectDir/.openapi-generator-ignore"
|
||||
configOptions = [
|
||||
dateLibrary : 'java8',
|
||||
library : 'feign',
|
||||
feignVersion : '13.6',
|
||||
feignApacheHttpClient : 'true',
|
||||
useFeign13 : 'true',
|
||||
useFeignApacheHttpClient : 'true',
|
||||
hideGenerationTimestamp : 'true',
|
||||
containerDefaultToNull : 'true',
|
||||
oauth2Implementation : 'none',
|
||||
useJakartaEe : 'true'
|
||||
|
||||
]
|
||||
dependsOn(':fineract-provider:resolve')
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
main {
|
||||
java {
|
||||
srcDirs = [
|
||||
new File(buildDir, "generated/java/src/main/java"),
|
||||
"$projectDir/src/main/java"
|
||||
]
|
||||
destinationDirectory = layout.buildDirectory.dir('classes/java/main').get().asFile
|
||||
}
|
||||
output.resourcesDir = layout.buildDirectory.dir('resources/main').get().asFile
|
||||
}
|
||||
test {
|
||||
java {
|
||||
destinationDirectory = layout.buildDirectory.dir('classes/java/test').get().asFile
|
||||
}
|
||||
output.resourcesDir = layout.buildDirectory.dir('resources/test').get().asFile
|
||||
}
|
||||
}
|
||||
|
||||
tasks.withType(Jar).configureEach {
|
||||
duplicatesStrategy = DuplicatesStrategy.EXCLUDE
|
||||
}
|
||||
|
||||
task cleanupGeneratedJavaFiles() {
|
||||
def tempDir = file("$buildDir/generated/temp-java")
|
||||
def targetDir = file("$buildDir/generated/java")
|
||||
|
||||
inputs.dir(tempDir)
|
||||
outputs.dir(targetDir)
|
||||
|
||||
doLast {
|
||||
copy {
|
||||
from tempDir
|
||||
into targetDir
|
||||
filter { line ->
|
||||
line
|
||||
.replaceAll(", \\)", ")")
|
||||
.replaceAll(", , @HeaderMap", ", @HeaderMap")
|
||||
.replaceAll("\\(, ", "(")
|
||||
}
|
||||
duplicatesStrategy = DuplicatesStrategy.EXCLUDE
|
||||
}
|
||||
}
|
||||
dependsOn("buildJavaSdk")
|
||||
}
|
||||
|
||||
tasks.named('compileJava') {
|
||||
outputs.cacheIf { true }
|
||||
dependsOn(buildJavaSdk, cleanupGeneratedJavaFiles, licenseFormatMain, spotlessMiscApply)
|
||||
mustRunAfter(licenseFormatMain, cleanupGeneratedJavaFiles)
|
||||
}
|
||||
|
||||
tasks.named('sourcesJar') {
|
||||
dependsOn(cleanupGeneratedJavaFiles)
|
||||
mustRunAfter(cleanupGeneratedJavaFiles)
|
||||
|
||||
from(sourceSets.main.java.srcDirs) {
|
||||
include "**/*.java"
|
||||
}
|
||||
}
|
||||
|
||||
tasks.named('licenseFormatMain') {
|
||||
dependsOn(cleanupGeneratedJavaFiles)
|
||||
mustRunAfter(cleanupGeneratedJavaFiles)
|
||||
source = sourceSets.main.java.srcDirs
|
||||
}
|
||||
|
||||
tasks.named('licenseMain') {
|
||||
dependsOn(licenseFormatMain)
|
||||
mustRunAfter(licenseFormatMain)
|
||||
}
|
||||
|
||||
tasks.withType(JavaCompile).configureEach {
|
||||
options.encoding = 'UTF-8'
|
||||
options.compilerArgs << '-parameters'
|
||||
options.errorprone {
|
||||
excludedPaths = '.*/build/generated/java/src/main/java/.*'
|
||||
}
|
||||
}
|
||||
|
||||
test {
|
||||
useJUnitPlatform()
|
||||
}
|
||||
|
||||
configurations {
|
||||
generatedCompileClasspath.extendsFrom implementation
|
||||
generatedRuntimeClasspath.extendsFrom runtimeClasspath
|
||||
}
|
||||
|
||||
javadoc {
|
||||
options.encoding = 'UTF-8'
|
||||
}
|
||||
|
||||
spotbugsMain {
|
||||
enabled = false
|
||||
}
|
||||
|
||||
spotbugsTest {
|
||||
enabled = false
|
||||
}
|
||||
|
||||
jacoco {
|
||||
toolVersion = "0.8.11"
|
||||
}
|
||||
|
||||
jacocoTestReport {
|
||||
dependsOn test
|
||||
reports {
|
||||
xml.required = true
|
||||
html.required = true
|
||||
csv.required = false
|
||||
}
|
||||
afterEvaluate {
|
||||
classDirectories.setFrom(files(classDirectories.files.collect {
|
||||
fileTree(dir: it, exclude: [
|
||||
'**/build/generated/**',
|
||||
'**/org/apache/fineract/client/models/**',
|
||||
'**/org/apache/fineract/client/services/**Api.class',
|
||||
'**/org/apache/fineract/client/auth/**'
|
||||
])
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
test {
|
||||
finalizedBy jacocoTestReport
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
dependencies {
|
||||
// Feign dependencies
|
||||
implementation(
|
||||
'io.github.openfeign:feign-core:13.6',
|
||||
'io.github.openfeign:feign-jackson:13.6',
|
||||
'io.github.openfeign:feign-slf4j:13.6',
|
||||
'io.github.openfeign:feign-hc5:13.6',
|
||||
'io.github.openfeign:feign-okhttp:13.6',
|
||||
'io.github.openfeign.form:feign-form:3.8.0',
|
||||
'org.apache.httpcomponents.client5:httpclient5:5.2.1',
|
||||
'com.squareup.okhttp3:okhttp:4.12.0',
|
||||
'com.fasterxml.jackson.core:jackson-databind',
|
||||
'com.fasterxml.jackson.datatype:jackson-datatype-jsr310',
|
||||
'com.fasterxml.jackson.datatype:jackson-datatype-jdk8',
|
||||
'jakarta.annotation:jakarta.annotation-api:3.0.0',
|
||||
'io.swagger.core.v3:swagger-annotations-jakarta:2.2.15',
|
||||
'org.apache.commons:commons-lang3:3.12.0',
|
||||
'org.slf4j:slf4j-api:1.7.36',
|
||||
'org.projectlombok:lombok'
|
||||
)
|
||||
|
||||
// Test dependencies
|
||||
testImplementation(
|
||||
'org.junit.jupiter:junit-jupiter-api:5.11.3',
|
||||
'org.junit.jupiter:junit-jupiter-engine:5.11.3',
|
||||
'org.mockito:mockito-core:5.14.2',
|
||||
'org.assertj:assertj-core:3.26.3',
|
||||
'org.slf4j:slf4j-simple:1.7.36',
|
||||
'org.wiremock:wiremock-standalone'
|
||||
)
|
||||
}
|
||||
+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}}
|
||||
}
|
||||
+192
@@ -0,0 +1,192 @@
|
||||
/**
|
||||
* 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 static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.apache.fineract.client.feign.ObjectMapperFactory;
|
||||
import org.apache.fineract.client.models.ExternalId;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
class ExternalIdAdapterTest {
|
||||
|
||||
private ObjectMapper mapper;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
mapper = ObjectMapperFactory.getShared();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSerializeExternalId() throws Exception {
|
||||
ExternalId externalId = new ExternalId().value("EXT-12345");
|
||||
|
||||
String json = mapper.writeValueAsString(externalId);
|
||||
|
||||
assertThat(json).isEqualTo("\"EXT-12345\"");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSerializeExternalIdWithSpecialCharacters() throws Exception {
|
||||
ExternalId externalId = new ExternalId().value("EXT-ID-2024/01/15");
|
||||
|
||||
String json = mapper.writeValueAsString(externalId);
|
||||
|
||||
assertThat(json).isEqualTo("\"EXT-ID-2024/01/15\"");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSerializeNullExternalId() throws Exception {
|
||||
ExternalId externalId = new ExternalId().value(null);
|
||||
|
||||
String json = mapper.writeValueAsString(externalId);
|
||||
|
||||
assertThat(json).isEqualTo("null");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSerializeExternalIdObjectNull() throws Exception {
|
||||
ExternalId externalId = null;
|
||||
|
||||
String json = mapper.writeValueAsString(externalId);
|
||||
|
||||
assertThat(json).isEqualTo("null");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testDeserializeExternalId() throws Exception {
|
||||
String json = "\"EXT-67890\"";
|
||||
|
||||
ExternalId externalId = mapper.readValue(json, ExternalId.class);
|
||||
|
||||
assertThat(externalId).isNotNull();
|
||||
assertThat(externalId.getValue()).isEqualTo("EXT-67890");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testDeserializeExternalIdWithSpecialCharacters() throws Exception {
|
||||
String json = "\"CLIENT-2024-001/ABC\"";
|
||||
|
||||
ExternalId externalId = mapper.readValue(json, ExternalId.class);
|
||||
|
||||
assertThat(externalId).isNotNull();
|
||||
assertThat(externalId.getValue()).isEqualTo("CLIENT-2024-001/ABC");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testDeserializeNullExternalId() throws Exception {
|
||||
String json = "null";
|
||||
|
||||
ExternalId externalId = mapper.readValue(json, ExternalId.class);
|
||||
|
||||
assertThat(externalId).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testRoundTripSerialization() throws Exception {
|
||||
ExternalId original = new ExternalId().value("TEST-ID-001");
|
||||
|
||||
String json = mapper.writeValueAsString(original);
|
||||
ExternalId deserialized = mapper.readValue(json, ExternalId.class);
|
||||
|
||||
assertThat(deserialized.getValue()).isEqualTo(original.getValue());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testRoundTripSerializationWithUUID() throws Exception {
|
||||
ExternalId original = new ExternalId().value("550e8400-e29b-41d4-a716-446655440000");
|
||||
|
||||
String json = mapper.writeValueAsString(original);
|
||||
ExternalId deserialized = mapper.readValue(json, ExternalId.class);
|
||||
|
||||
assertThat(deserialized.getValue()).isEqualTo(original.getValue());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testRoundTripSerializationWithNumericString() throws Exception {
|
||||
ExternalId original = new ExternalId().value("123456789");
|
||||
|
||||
String json = mapper.writeValueAsString(original);
|
||||
ExternalId deserialized = mapper.readValue(json, ExternalId.class);
|
||||
|
||||
assertThat(deserialized.getValue()).isEqualTo(original.getValue());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testDeserializeEmptyString() throws Exception {
|
||||
String json = "\"\"";
|
||||
|
||||
ExternalId externalId = mapper.readValue(json, ExternalId.class);
|
||||
|
||||
assertThat(externalId).isNotNull();
|
||||
assertThat(externalId.getValue()).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSerializeExternalIdWithEmptyValue() throws Exception {
|
||||
ExternalId externalId = new ExternalId().value("");
|
||||
|
||||
String json = mapper.writeValueAsString(externalId);
|
||||
|
||||
assertThat(json).isEqualTo("\"\"");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testModuleCreation() {
|
||||
assertThat(ExternalIdAdapter.createModule()).isNotNull();
|
||||
assertThat(ExternalIdAdapter.createModule().getModuleName()).isEqualTo("ExternalIdModule");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testDeserializeExternalIdWithWhitespace() throws Exception {
|
||||
String json = "\" EXT-001 \"";
|
||||
|
||||
ExternalId externalId = mapper.readValue(json, ExternalId.class);
|
||||
|
||||
assertThat(externalId).isNotNull();
|
||||
assertThat(externalId.getValue()).isEqualTo(" EXT-001 ");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSerializeExternalIdInObject() throws Exception {
|
||||
TestObject obj = new TestObject();
|
||||
obj.externalId = new ExternalId().value("TEST-123");
|
||||
|
||||
String json = mapper.writeValueAsString(obj);
|
||||
|
||||
assertThat(json).contains("\"externalId\":\"TEST-123\"");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testDeserializeExternalIdInObject() throws Exception {
|
||||
String json = "{\"externalId\":\"TEST-456\"}";
|
||||
|
||||
TestObject obj = mapper.readValue(json, TestObject.class);
|
||||
|
||||
assertThat(obj.externalId).isNotNull();
|
||||
assertThat(obj.externalId.getValue()).isEqualTo("TEST-456");
|
||||
}
|
||||
|
||||
static class TestObject {
|
||||
|
||||
public ExternalId externalId;
|
||||
}
|
||||
}
|
||||
+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;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
import feign.Request;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Collections;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
class FeignExceptionTest {
|
||||
|
||||
private Request request;
|
||||
private byte[] responseBody;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
request = Request.create(Request.HttpMethod.GET, "/api/test", Collections.emptyMap(), null, StandardCharsets.UTF_8, null);
|
||||
responseBody = "Error response body".getBytes(StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testConstructorWithErrorDetails() {
|
||||
String developerMessage = "Technical error details";
|
||||
String userMessage = "User-friendly error message";
|
||||
|
||||
FeignException exception = new FeignException(400, "Error message", request, responseBody, developerMessage, userMessage);
|
||||
|
||||
assertNotNull(exception);
|
||||
assertEquals(400, exception.status());
|
||||
assertEquals(developerMessage, exception.getDeveloperMessage());
|
||||
assertEquals(userMessage, exception.getUserMessage());
|
||||
assertEquals(request, exception.request());
|
||||
assertEquals(responseBody, exception.responseBody());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetMessage() {
|
||||
String developerMessage = "Developer error";
|
||||
String userMessage = "User error";
|
||||
|
||||
FeignException exception = new FeignException(500, "Base message", request, responseBody, developerMessage, userMessage);
|
||||
|
||||
String message = exception.getMessage();
|
||||
assertNotNull(message);
|
||||
assertTrue(message.contains("500"));
|
||||
assertTrue(message.contains(userMessage));
|
||||
assertTrue(message.contains(developerMessage));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetDeveloperMessage() {
|
||||
String developerMessage = "This is a technical error";
|
||||
|
||||
FeignException exception = new FeignException(400, "Error", request, responseBody, developerMessage, null);
|
||||
|
||||
assertEquals(developerMessage, exception.getDeveloperMessage());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetUserMessage() {
|
||||
String userMessage = "This is a user-friendly error";
|
||||
|
||||
FeignException exception = new FeignException(400, "Error", request, responseBody, null, userMessage);
|
||||
|
||||
assertEquals(userMessage, exception.getUserMessage());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testNullErrorMessages() {
|
||||
FeignException exception = new FeignException(404, "Not found", request, responseBody, null, null);
|
||||
|
||||
assertNull(exception.getDeveloperMessage());
|
||||
assertNull(exception.getUserMessage());
|
||||
String message = exception.getMessage();
|
||||
assertNotNull(message);
|
||||
assertTrue(message.contains("404"));
|
||||
assertTrue(message.contains("Not found"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetMessageWithOnlyUserMessage() {
|
||||
String userMessage = "User error only";
|
||||
|
||||
FeignException exception = new FeignException(400, "Base message", request, responseBody, null, userMessage);
|
||||
|
||||
String message = exception.getMessage();
|
||||
assertTrue(message.contains("400"));
|
||||
assertTrue(message.contains(userMessage));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetMessageWithOnlyDeveloperMessage() {
|
||||
String developerMessage = "Developer error only";
|
||||
|
||||
FeignException exception = new FeignException(400, "Base message", request, responseBody, developerMessage, null);
|
||||
|
||||
String message = exception.getMessage();
|
||||
assertTrue(message.contains("400"));
|
||||
assertTrue(message.contains(developerMessage));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testResponseBodyAsString() {
|
||||
FeignException exception = new FeignException(400, "Error", request, responseBody, null, null);
|
||||
|
||||
String bodyString = exception.responseBodyAsString();
|
||||
assertNotNull(bodyString);
|
||||
assertEquals("Error response body", bodyString);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testResponseBodyAsStringWithNullBody() {
|
||||
FeignException exception = new FeignException(400, "Error", request, null, null, null);
|
||||
|
||||
String bodyString = exception.responseBodyAsString();
|
||||
assertNull(bodyString);
|
||||
}
|
||||
}
|
||||
+180
@@ -0,0 +1,180 @@
|
||||
/**
|
||||
* 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 static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
|
||||
import feign.Request;
|
||||
import feign.Response;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
class FineractErrorDecoderTest {
|
||||
|
||||
private FineractErrorDecoder decoder;
|
||||
private Request request;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
decoder = new FineractErrorDecoder();
|
||||
request = Request.create(Request.HttpMethod.GET, "/api/test", Collections.emptyMap(), null, StandardCharsets.UTF_8, null);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testDecodeValidFineractError() {
|
||||
String jsonError = "{\"developerMessage\":\"Developer error message\",\"userMessage\":\"User error message\"}";
|
||||
Response response = createResponse(400, jsonError);
|
||||
|
||||
Exception exception = decoder.decode("Test#method", response);
|
||||
|
||||
assertInstanceOf(FeignException.class, exception);
|
||||
FeignException feignException = (FeignException) exception;
|
||||
assertEquals(400, feignException.status());
|
||||
assertEquals("User error message", feignException.getUserMessage());
|
||||
assertEquals("Developer error message", feignException.getDeveloperMessage());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testDecodeInvalidJson() {
|
||||
String invalidJson = "This is not valid JSON";
|
||||
Response response = createResponse(400, invalidJson);
|
||||
|
||||
Exception exception = decoder.decode("Test#method", response);
|
||||
|
||||
assertNotNull(exception);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testDecodeNullBody() {
|
||||
Response response = createResponse(404, null);
|
||||
|
||||
Exception exception = decoder.decode("Test#method", response);
|
||||
|
||||
assertNotNull(exception);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testDecode400Error() {
|
||||
String jsonError = "{\"developerMessage\":\"Bad request details\",\"userMessage\":\"Invalid input\"}";
|
||||
Response response = createResponse(400, jsonError);
|
||||
|
||||
Exception exception = decoder.decode("Test#method", response);
|
||||
|
||||
assertInstanceOf(FeignException.class, exception);
|
||||
FeignException feignException = (FeignException) exception;
|
||||
assertEquals(400, feignException.status());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testDecode404Error() {
|
||||
String jsonError = "{\"developerMessage\":\"Resource not found details\",\"userMessage\":\"Not found\"}";
|
||||
Response response = createResponse(404, jsonError);
|
||||
|
||||
Exception exception = decoder.decode("Test#method", response);
|
||||
|
||||
assertInstanceOf(FeignException.class, exception);
|
||||
FeignException feignException = (FeignException) exception;
|
||||
assertEquals(404, feignException.status());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testDecode500Error() {
|
||||
String jsonError = "{\"developerMessage\":\"Internal server error details\",\"userMessage\":\"Server error\"}";
|
||||
Response response = createResponse(500, jsonError);
|
||||
|
||||
Exception exception = decoder.decode("Test#method", response);
|
||||
|
||||
assertInstanceOf(FeignException.class, exception);
|
||||
FeignException feignException = (FeignException) exception;
|
||||
assertEquals(500, feignException.status());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testExtractDeveloperMessage() {
|
||||
String jsonError = "{\"developerMessage\":\"Technical details here\"}";
|
||||
Response response = createResponse(400, jsonError);
|
||||
|
||||
Exception exception = decoder.decode("Test#method", response);
|
||||
|
||||
assertInstanceOf(FeignException.class, exception);
|
||||
FeignException feignException = (FeignException) exception;
|
||||
assertEquals("Technical details here", feignException.getDeveloperMessage());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testExtractUserMessage() {
|
||||
String jsonError = "{\"userMessage\":\"User-friendly message\"}";
|
||||
Response response = createResponse(400, jsonError);
|
||||
|
||||
Exception exception = decoder.decode("Test#method", response);
|
||||
|
||||
assertInstanceOf(FeignException.class, exception);
|
||||
FeignException feignException = (FeignException) exception;
|
||||
assertEquals("User-friendly message", feignException.getUserMessage());
|
||||
}
|
||||
|
||||
private Response createResponse(int status, String body) {
|
||||
Map<String, Collection<String>> headers = new HashMap<>();
|
||||
|
||||
Response.Body responseBody = null;
|
||||
if (body != null) {
|
||||
byte[] bodyBytes = body.getBytes(StandardCharsets.UTF_8);
|
||||
responseBody = new Response.Body() {
|
||||
|
||||
@Override
|
||||
public Integer length() {
|
||||
return bodyBytes.length;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isRepeatable() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ByteArrayInputStream asInputStream() {
|
||||
return new ByteArrayInputStream(bodyBytes);
|
||||
}
|
||||
|
||||
@Override
|
||||
public java.io.Reader asReader() {
|
||||
return new java.io.InputStreamReader(asInputStream(), StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
@Override
|
||||
public java.io.Reader asReader(java.nio.charset.Charset charset) {
|
||||
return new java.io.InputStreamReader(asInputStream(), charset);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {}
|
||||
};
|
||||
}
|
||||
|
||||
return Response.builder().status(status).reason("Test").request(request).headers(headers).body(responseBody).build();
|
||||
}
|
||||
}
|
||||
+93
@@ -0,0 +1,93 @@
|
||||
/**
|
||||
* 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 static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
|
||||
import feign.RequestLine;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
class FineractFeignClientConfigTest {
|
||||
|
||||
@Test
|
||||
void testBuilderDefaults() {
|
||||
FineractFeignClientConfig.Builder builder = FineractFeignClientConfig.builder();
|
||||
FineractFeignClientConfig config = builder.baseUrl("http://localhost:8080").credentials("admin", "password").build();
|
||||
|
||||
assertNotNull(config);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testBuilderConfiguration() {
|
||||
String baseUrl = "http://example.com:8080";
|
||||
String username = "testuser";
|
||||
String password = "testpass";
|
||||
int connectTimeoutSeconds = 10;
|
||||
int readTimeoutSeconds = 30;
|
||||
|
||||
FineractFeignClientConfig config = FineractFeignClientConfig.builder().baseUrl(baseUrl).credentials(username, password)
|
||||
.connectTimeout(connectTimeoutSeconds, TimeUnit.SECONDS).readTimeout(readTimeoutSeconds, TimeUnit.SECONDS)
|
||||
.debugEnabled(true).build();
|
||||
|
||||
assertNotNull(config);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testConnectionTimeToLiveConfiguration() {
|
||||
long ttl = 5;
|
||||
TimeUnit ttlUnit = TimeUnit.MINUTES;
|
||||
|
||||
FineractFeignClientConfig config = FineractFeignClientConfig.builder().baseUrl("http://localhost:8080")
|
||||
.credentials("admin", "password").connectionTimeToLive(ttl, ttlUnit).build();
|
||||
|
||||
assertNotNull(config);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testEncoderDecoderConfiguration() {
|
||||
FineractFeignClientConfig config = FineractFeignClientConfig.builder().baseUrl("http://localhost:8080")
|
||||
.credentials("admin", "password").build();
|
||||
|
||||
assertNotNull(config);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testErrorDecoderConfiguration() {
|
||||
FineractFeignClientConfig config = FineractFeignClientConfig.builder().baseUrl("http://localhost:8080")
|
||||
.credentials("admin", "password").build();
|
||||
|
||||
assertNotNull(config);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testClientCreation() {
|
||||
FineractFeignClientConfig config = FineractFeignClientConfig.builder().baseUrl("http://localhost:8080")
|
||||
.credentials("admin", "password").connectTimeout(5, TimeUnit.SECONDS).readTimeout(10, TimeUnit.SECONDS).build();
|
||||
|
||||
TestApi client = config.createClient(TestApi.class);
|
||||
assertNotNull(client);
|
||||
}
|
||||
|
||||
interface TestApi {
|
||||
|
||||
@RequestLine("GET /test")
|
||||
String test();
|
||||
}
|
||||
}
|
||||
+120
@@ -0,0 +1,120 @@
|
||||
/**
|
||||
* 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 static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertSame;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import java.io.IOException;
|
||||
import java.time.LocalDate;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
class ObjectMapperFactoryTest {
|
||||
|
||||
@Test
|
||||
void testSharedObjectMapperNotNull() {
|
||||
ObjectMapper mapper = ObjectMapperFactory.getShared();
|
||||
assertNotNull(mapper);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSharedObjectMapperSingleton() {
|
||||
ObjectMapper mapper1 = ObjectMapperFactory.getShared();
|
||||
ObjectMapper mapper2 = ObjectMapperFactory.getShared();
|
||||
assertSame(mapper1, mapper2);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testJava8DateTimeSerialization() throws JsonProcessingException {
|
||||
ObjectMapper mapper = ObjectMapperFactory.getShared();
|
||||
LocalDate date = LocalDate.of(2024, 1, 15);
|
||||
|
||||
String json = mapper.writeValueAsString(new TestObject(date));
|
||||
assertNotNull(json);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testJava8DateTimeDeserialization() throws IOException {
|
||||
ObjectMapper mapper = ObjectMapperFactory.getShared();
|
||||
String json = "{\"date\":\"2024-01-15\"}";
|
||||
|
||||
TestObject obj = mapper.readValue(json, TestObject.class);
|
||||
assertNotNull(obj);
|
||||
assertNotNull(obj.getDate());
|
||||
assertEquals(LocalDate.of(2024, 1, 15), obj.getDate());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testNullHandling() throws JsonProcessingException {
|
||||
ObjectMapper mapper = ObjectMapperFactory.getShared();
|
||||
TestObject obj = new TestObject(null);
|
||||
|
||||
String json = mapper.writeValueAsString(obj);
|
||||
assertNotNull(json);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testDeserializeNullValue() throws IOException {
|
||||
ObjectMapper mapper = ObjectMapperFactory.getShared();
|
||||
String json = "{\"date\":null}";
|
||||
|
||||
TestObject obj = mapper.readValue(json, TestObject.class);
|
||||
assertNotNull(obj);
|
||||
assertNull(obj.getDate());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testUnknownPropertiesIgnored() throws IOException {
|
||||
ObjectMapper mapper = ObjectMapperFactory.getShared();
|
||||
String json = "{\"date\":\"2024-01-15\",\"unknownField\":\"value\"}";
|
||||
|
||||
TestObject obj = mapper.readValue(json, TestObject.class);
|
||||
assertNotNull(obj);
|
||||
assertEquals(LocalDate.of(2024, 1, 15), obj.getDate());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCreateObjectMapperNotNull() {
|
||||
ObjectMapper mapper = ObjectMapperFactory.createObjectMapper();
|
||||
assertNotNull(mapper);
|
||||
}
|
||||
|
||||
static class TestObject {
|
||||
|
||||
private LocalDate date;
|
||||
|
||||
TestObject() {}
|
||||
|
||||
TestObject(LocalDate date) {
|
||||
this.date = date;
|
||||
}
|
||||
|
||||
public LocalDate getDate() {
|
||||
return date;
|
||||
}
|
||||
|
||||
public void setDate(LocalDate date) {
|
||||
this.date = date;
|
||||
}
|
||||
}
|
||||
}
|
||||
+421
@@ -0,0 +1,421 @@
|
||||
/**
|
||||
* 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.integration;
|
||||
|
||||
import static com.github.tomakehurst.wiremock.client.WireMock.aResponse;
|
||||
import static com.github.tomakehurst.wiremock.client.WireMock.post;
|
||||
import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
import com.github.tomakehurst.wiremock.WireMockServer;
|
||||
import com.github.tomakehurst.wiremock.core.WireMockConfiguration;
|
||||
import feign.RequestLine;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.ZoneId;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import org.apache.fineract.client.feign.FineractFeignClientConfig;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Tag;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
@Tag("integration")
|
||||
class EncoderDecoderIntegrationTest {
|
||||
|
||||
private WireMockServer wireMockServer;
|
||||
private String baseUrl;
|
||||
private FineractFeignClientConfig config;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
wireMockServer = new WireMockServer(WireMockConfiguration.wireMockConfig().dynamicPort());
|
||||
wireMockServer.start();
|
||||
baseUrl = "http://localhost:" + wireMockServer.port();
|
||||
|
||||
config = FineractFeignClientConfig.builder().baseUrl(baseUrl).credentials("testuser", "testpass")
|
||||
.connectTimeout(5, TimeUnit.SECONDS).readTimeout(10, TimeUnit.SECONDS).build();
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
void tearDown() {
|
||||
if (wireMockServer != null) {
|
||||
wireMockServer.stop();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void testComplexObjectSerialization() {
|
||||
wireMockServer.stubFor(post(urlEqualTo("/complex")).willReturn(aResponse().withStatus(201)
|
||||
.withHeader("Content-Type", "application/json").withBody("{\"id\":100,\"status\":\"created\"}")));
|
||||
|
||||
ComplexApi client = config.createClient(ComplexApi.class);
|
||||
|
||||
ComplexRequest request = new ComplexRequest();
|
||||
request.setName("complex-test");
|
||||
request.setAmount(new BigDecimal("1234.56"));
|
||||
request.setCreatedDate(LocalDate.of(2024, 1, 15));
|
||||
request.setItems(Arrays.asList("item1", "item2", "item3"));
|
||||
|
||||
Map<String, String> metadata = new HashMap<>();
|
||||
metadata.put("key1", "value1");
|
||||
metadata.put("key2", "value2");
|
||||
request.setMetadata(metadata);
|
||||
|
||||
NestedObject nested = new NestedObject();
|
||||
nested.setNestedName("nested-value");
|
||||
nested.setNestedDate(LocalDate.of(2024, 2, 20));
|
||||
request.setNested(nested);
|
||||
|
||||
SimpleResponse response = client.createComplex(request);
|
||||
|
||||
assertThat(response).isNotNull();
|
||||
assertThat(response.getId()).isEqualTo(100L);
|
||||
assertThat(response.getStatus()).isEqualTo("created");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testNullValueHandling() {
|
||||
wireMockServer.stubFor(post(urlEqualTo("/complex")).willReturn(
|
||||
aResponse().withStatus(200).withHeader("Content-Type", "application/json").withBody("{\"id\":1,\"status\":null}")));
|
||||
|
||||
ComplexApi client = config.createClient(ComplexApi.class);
|
||||
|
||||
ComplexRequest request = new ComplexRequest();
|
||||
request.setName("null-test");
|
||||
request.setAmount(null);
|
||||
request.setCreatedDate(null);
|
||||
request.setItems(null);
|
||||
request.setMetadata(null);
|
||||
request.setNested(null);
|
||||
|
||||
SimpleResponse response = client.createComplex(request);
|
||||
|
||||
assertThat(response).isNotNull();
|
||||
assertThat(response.getId()).isEqualTo(1L);
|
||||
assertThat(response.getStatus()).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testDateFormats() {
|
||||
String responseJson = "{\"id\":1,\"date\":\"2024-03-15\",\"dateTime\":\"2024-03-15T14:30:00\",\"timestamp\":\"2024-03-15T14:30:00.123\"}";
|
||||
|
||||
wireMockServer.stubFor(post(urlEqualTo("/date-test"))
|
||||
.willReturn(aResponse().withStatus(200).withHeader("Content-Type", "application/json").withBody(responseJson)));
|
||||
|
||||
ComplexApi client = config.createClient(ComplexApi.class);
|
||||
|
||||
ComplexRequest request = new ComplexRequest();
|
||||
request.setName("date-test");
|
||||
request.setCreatedDate(LocalDate.of(2024, 3, 15));
|
||||
|
||||
DateResponse response = client.testDates(request);
|
||||
|
||||
assertThat(response).isNotNull();
|
||||
assertThat(response.getDate()).isEqualTo(LocalDate.of(2024, 3, 15));
|
||||
assertThat(response.getDateTime()).isEqualTo(LocalDateTime.of(2024, 3, 15, 14, 30, 0));
|
||||
assertThat(response.getTimestamp()).isNotNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testLargePayload() {
|
||||
StringBuilder largeString = new StringBuilder();
|
||||
for (int i = 0; i < 1000; i++) {
|
||||
largeString.append("This is line ").append(i).append(". ");
|
||||
}
|
||||
|
||||
wireMockServer.stubFor(post(urlEqualTo("/complex")).willReturn(aResponse().withStatus(200)
|
||||
.withHeader("Content-Type", "application/json").withBody("{\"id\":1,\"status\":\"processed\"}")));
|
||||
|
||||
ComplexApi client = config.createClient(ComplexApi.class);
|
||||
|
||||
ComplexRequest request = new ComplexRequest();
|
||||
request.setName(largeString.toString());
|
||||
request.setAmount(new BigDecimal("999999.99"));
|
||||
request.setCreatedDate(LocalDate.now(ZoneId.systemDefault()));
|
||||
|
||||
SimpleResponse response = client.createComplex(request);
|
||||
|
||||
assertThat(response).isNotNull();
|
||||
assertThat(response.getStatus()).isEqualTo("processed");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testEmptyCollections() {
|
||||
wireMockServer.stubFor(post(urlEqualTo("/complex")).willReturn(
|
||||
aResponse().withStatus(200).withHeader("Content-Type", "application/json").withBody("{\"id\":1,\"status\":\"ok\"}")));
|
||||
|
||||
ComplexApi client = config.createClient(ComplexApi.class);
|
||||
|
||||
ComplexRequest request = new ComplexRequest();
|
||||
request.setName("empty-test");
|
||||
request.setItems(Arrays.asList());
|
||||
request.setMetadata(new HashMap<>());
|
||||
|
||||
SimpleResponse response = client.createComplex(request);
|
||||
|
||||
assertThat(response).isNotNull();
|
||||
assertThat(response.getStatus()).isEqualTo("ok");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testBigDecimalPrecision() {
|
||||
wireMockServer.stubFor(post(urlEqualTo("/complex")).willReturn(
|
||||
aResponse().withStatus(200).withHeader("Content-Type", "application/json").withBody("{\"id\":1,\"status\":\"precise\"}")));
|
||||
|
||||
ComplexApi client = config.createClient(ComplexApi.class);
|
||||
|
||||
ComplexRequest request = new ComplexRequest();
|
||||
request.setName("precision-test");
|
||||
request.setAmount(new BigDecimal("123456789.123456789"));
|
||||
|
||||
SimpleResponse response = client.createComplex(request);
|
||||
|
||||
assertThat(response).isNotNull();
|
||||
assertThat(response.getStatus()).isEqualTo("precise");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testNestedObjectDeserialization() {
|
||||
String responseJson = "{\"id\":1,\"nested\":{\"nestedName\":\"deep-value\",\"nestedDate\":\"2024-04-01\"},\"items\":[\"a\",\"b\",\"c\"]}";
|
||||
|
||||
wireMockServer.stubFor(post(urlEqualTo("/nested"))
|
||||
.willReturn(aResponse().withStatus(200).withHeader("Content-Type", "application/json").withBody(responseJson)));
|
||||
|
||||
ComplexApi client = config.createClient(ComplexApi.class);
|
||||
|
||||
ComplexRequest request = new ComplexRequest();
|
||||
request.setName("nested-test");
|
||||
|
||||
NestedResponse response = client.testNested(request);
|
||||
|
||||
assertThat(response).isNotNull();
|
||||
assertThat(response.getId()).isEqualTo(1L);
|
||||
assertThat(response.getNested()).isNotNull();
|
||||
assertThat(response.getNested().getNestedName()).isEqualTo("deep-value");
|
||||
assertThat(response.getNested().getNestedDate()).isEqualTo(LocalDate.of(2024, 4, 1));
|
||||
assertThat(response.getItems()).containsExactly("a", "b", "c");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSpecialCharacterHandling() {
|
||||
wireMockServer.stubFor(post(urlEqualTo("/complex")).willReturn(
|
||||
aResponse().withStatus(200).withHeader("Content-Type", "application/json").withBody("{\"id\":1,\"status\":\"special\"}")));
|
||||
|
||||
ComplexApi client = config.createClient(ComplexApi.class);
|
||||
|
||||
ComplexRequest request = new ComplexRequest();
|
||||
request.setName("Test with special chars: <>\"'&\n\t");
|
||||
|
||||
SimpleResponse response = client.createComplex(request);
|
||||
|
||||
assertThat(response).isNotNull();
|
||||
assertThat(response.getStatus()).isEqualTo("special");
|
||||
}
|
||||
|
||||
interface ComplexApi {
|
||||
|
||||
@RequestLine("POST /complex")
|
||||
SimpleResponse createComplex(ComplexRequest request);
|
||||
|
||||
@RequestLine("POST /date-test")
|
||||
DateResponse testDates(ComplexRequest request);
|
||||
|
||||
@RequestLine("POST /nested")
|
||||
NestedResponse testNested(ComplexRequest request);
|
||||
}
|
||||
|
||||
static class ComplexRequest {
|
||||
|
||||
private String name;
|
||||
private BigDecimal amount;
|
||||
private LocalDate createdDate;
|
||||
private List<String> items;
|
||||
private Map<String, String> metadata;
|
||||
private NestedObject nested;
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public BigDecimal getAmount() {
|
||||
return amount;
|
||||
}
|
||||
|
||||
public void setAmount(BigDecimal amount) {
|
||||
this.amount = amount;
|
||||
}
|
||||
|
||||
public LocalDate getCreatedDate() {
|
||||
return createdDate;
|
||||
}
|
||||
|
||||
public void setCreatedDate(LocalDate createdDate) {
|
||||
this.createdDate = createdDate;
|
||||
}
|
||||
|
||||
public List<String> getItems() {
|
||||
return items;
|
||||
}
|
||||
|
||||
public void setItems(List<String> items) {
|
||||
this.items = items;
|
||||
}
|
||||
|
||||
public Map<String, String> getMetadata() {
|
||||
return metadata;
|
||||
}
|
||||
|
||||
public void setMetadata(Map<String, String> metadata) {
|
||||
this.metadata = metadata;
|
||||
}
|
||||
|
||||
public NestedObject getNested() {
|
||||
return nested;
|
||||
}
|
||||
|
||||
public void setNested(NestedObject nested) {
|
||||
this.nested = nested;
|
||||
}
|
||||
}
|
||||
|
||||
static class NestedObject {
|
||||
|
||||
private String nestedName;
|
||||
private LocalDate nestedDate;
|
||||
|
||||
public String getNestedName() {
|
||||
return nestedName;
|
||||
}
|
||||
|
||||
public void setNestedName(String nestedName) {
|
||||
this.nestedName = nestedName;
|
||||
}
|
||||
|
||||
public LocalDate getNestedDate() {
|
||||
return nestedDate;
|
||||
}
|
||||
|
||||
public void setNestedDate(LocalDate nestedDate) {
|
||||
this.nestedDate = nestedDate;
|
||||
}
|
||||
}
|
||||
|
||||
static class SimpleResponse {
|
||||
|
||||
private Long id;
|
||||
private String status;
|
||||
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(Long id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public String getStatus() {
|
||||
return status;
|
||||
}
|
||||
|
||||
public void setStatus(String status) {
|
||||
this.status = status;
|
||||
}
|
||||
}
|
||||
|
||||
static class DateResponse {
|
||||
|
||||
private Long id;
|
||||
private LocalDate date;
|
||||
private LocalDateTime dateTime;
|
||||
private LocalDateTime timestamp;
|
||||
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(Long id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public LocalDate getDate() {
|
||||
return date;
|
||||
}
|
||||
|
||||
public void setDate(LocalDate date) {
|
||||
this.date = date;
|
||||
}
|
||||
|
||||
public LocalDateTime getDateTime() {
|
||||
return dateTime;
|
||||
}
|
||||
|
||||
public void setDateTime(LocalDateTime dateTime) {
|
||||
this.dateTime = dateTime;
|
||||
}
|
||||
|
||||
public LocalDateTime getTimestamp() {
|
||||
return timestamp;
|
||||
}
|
||||
|
||||
public void setTimestamp(LocalDateTime timestamp) {
|
||||
this.timestamp = timestamp;
|
||||
}
|
||||
}
|
||||
|
||||
static class NestedResponse {
|
||||
|
||||
private Long id;
|
||||
private NestedObject nested;
|
||||
private List<String> items;
|
||||
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(Long id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public NestedObject getNested() {
|
||||
return nested;
|
||||
}
|
||||
|
||||
public void setNested(NestedObject nested) {
|
||||
this.nested = nested;
|
||||
}
|
||||
|
||||
public List<String> getItems() {
|
||||
return items;
|
||||
}
|
||||
|
||||
public void setItems(List<String> items) {
|
||||
this.items = items;
|
||||
}
|
||||
}
|
||||
}
|
||||
+252
@@ -0,0 +1,252 @@
|
||||
/**
|
||||
* 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.integration;
|
||||
|
||||
import static com.github.tomakehurst.wiremock.client.WireMock.aResponse;
|
||||
import static com.github.tomakehurst.wiremock.client.WireMock.containing;
|
||||
import static com.github.tomakehurst.wiremock.client.WireMock.equalTo;
|
||||
import static com.github.tomakehurst.wiremock.client.WireMock.get;
|
||||
import static com.github.tomakehurst.wiremock.client.WireMock.post;
|
||||
import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
|
||||
import com.github.tomakehurst.wiremock.WireMockServer;
|
||||
import com.github.tomakehurst.wiremock.core.WireMockConfiguration;
|
||||
import feign.RequestLine;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import org.apache.fineract.client.feign.FeignException;
|
||||
import org.apache.fineract.client.feign.FineractFeignClientConfig;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Tag;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
@Tag("integration")
|
||||
class FineractFeignClientIntegrationTest {
|
||||
|
||||
private WireMockServer wireMockServer;
|
||||
private String baseUrl;
|
||||
private FineractFeignClientConfig config;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
wireMockServer = new WireMockServer(WireMockConfiguration.wireMockConfig().dynamicPort());
|
||||
wireMockServer.start();
|
||||
baseUrl = "http://localhost:" + wireMockServer.port();
|
||||
|
||||
config = FineractFeignClientConfig.builder().baseUrl(baseUrl).credentials("testuser", "testpass")
|
||||
.connectTimeout(5, TimeUnit.SECONDS).readTimeout(10, TimeUnit.SECONDS).build();
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
void tearDown() {
|
||||
if (wireMockServer != null) {
|
||||
wireMockServer.stop();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void testJsonEncoding() {
|
||||
wireMockServer
|
||||
.stubFor(post(urlEqualTo("/test")).willReturn(aResponse().withStatus(200).withHeader("Content-Type", "application/json")
|
||||
.withBody("{\"id\":1,\"message\":\"success\",\"timestamp\":\"2024-01-15T10:30:00\"}")));
|
||||
|
||||
TestApi client = config.createClient(TestApi.class);
|
||||
TestRequest request = new TestRequest();
|
||||
request.setName("test-name");
|
||||
request.setDate(LocalDate.of(2024, 1, 15));
|
||||
|
||||
TestResponse response = client.createTest(request);
|
||||
|
||||
assertThat(response).isNotNull();
|
||||
assertThat(response.getId()).isEqualTo(1L);
|
||||
assertThat(response.getMessage()).isEqualTo("success");
|
||||
assertThat(response.getTimestamp()).isEqualTo(LocalDateTime.of(2024, 1, 15, 10, 30, 0));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testJsonDecoding() {
|
||||
wireMockServer
|
||||
.stubFor(get(urlEqualTo("/test")).willReturn(aResponse().withStatus(200).withHeader("Content-Type", "application/json")
|
||||
.withBody("{\"id\":42,\"message\":\"decoded-message\",\"timestamp\":\"2024-12-25T18:45:30\"}")));
|
||||
|
||||
TestApi client = config.createClient(TestApi.class);
|
||||
TestResponse response = client.getTest();
|
||||
|
||||
assertThat(response).isNotNull();
|
||||
assertThat(response.getId()).isEqualTo(42L);
|
||||
assertThat(response.getMessage()).isEqualTo("decoded-message");
|
||||
assertThat(response.getTimestamp()).isEqualTo(LocalDateTime.of(2024, 12, 25, 18, 45, 30));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testJava8DateTimeSerialization() {
|
||||
wireMockServer.stubFor(post(urlEqualTo("/test")).withRequestBody(containing("2024-06-30"))
|
||||
.willReturn(aResponse().withStatus(200).withHeader("Content-Type", "application/json")
|
||||
.withBody("{\"id\":99,\"message\":\"date-test\",\"timestamp\":\"2024-06-30T12:00:00\"}")));
|
||||
|
||||
TestApi client = config.createClient(TestApi.class);
|
||||
TestRequest request = new TestRequest();
|
||||
request.setName("date-test");
|
||||
request.setDate(LocalDate.of(2024, 6, 30));
|
||||
|
||||
TestResponse response = client.createTest(request);
|
||||
|
||||
assertThat(response).isNotNull();
|
||||
assertThat(response.getId()).isEqualTo(99L);
|
||||
assertThat(response.getTimestamp()).isNotNull();
|
||||
assertThat(response.getTimestamp().toLocalDate()).isEqualTo(LocalDate.of(2024, 6, 30));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testErrorDecoder() {
|
||||
wireMockServer.stubFor(
|
||||
get(urlEqualTo("/error")).willReturn(aResponse().withStatus(404).withHeader("Content-Type", "application/json").withBody(
|
||||
"{\"developerMessage\":\"Resource not found\",\"httpStatusCode\":\"404\",\"defaultUserMessage\":\"Not Found\"}")));
|
||||
|
||||
TestApi client = config.createClient(TestApi.class);
|
||||
|
||||
assertThatThrownBy(client::getError).isInstanceOf(FeignException.class).hasMessageContaining("Resource not found");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testErrorDecoderWithServerError() {
|
||||
wireMockServer.stubFor(
|
||||
get(urlEqualTo("/error")).willReturn(aResponse().withStatus(500).withHeader("Content-Type", "application/json").withBody(
|
||||
"{\"developerMessage\":\"Internal server error occurred\",\"httpStatusCode\":\"500\",\"defaultUserMessage\":\"Server Error\"}")));
|
||||
|
||||
TestApi client = config.createClient(TestApi.class);
|
||||
|
||||
assertThatThrownBy(client::getError).isInstanceOf(FeignException.class).hasMessageContaining("Internal server error occurred");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testConnectionTimeToLiveConfiguration() {
|
||||
FineractFeignClientConfig ttlConfig = FineractFeignClientConfig.builder().baseUrl(baseUrl).credentials("testuser", "testpass")
|
||||
.connectionTimeToLive(5, TimeUnit.MINUTES).build();
|
||||
|
||||
wireMockServer
|
||||
.stubFor(get(urlEqualTo("/test")).willReturn(aResponse().withStatus(200).withHeader("Content-Type", "application/json")
|
||||
.withBody("{\"id\":1,\"message\":\"ttl-test\",\"timestamp\":\"2024-01-01T00:00:00\"}")));
|
||||
|
||||
TestApi client = ttlConfig.createClient(TestApi.class);
|
||||
TestResponse response = client.getTest();
|
||||
|
||||
assertThat(response).isNotNull();
|
||||
assertThat(response.getMessage()).isEqualTo("ttl-test");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testBasicAuthentication() {
|
||||
wireMockServer.stubFor(get(urlEqualTo("/test")).withHeader("Authorization", equalTo("Basic dGVzdHVzZXI6dGVzdHBhc3M="))
|
||||
.willReturn(aResponse().withStatus(200).withHeader("Content-Type", "application/json")
|
||||
.withBody("{\"id\":1,\"message\":\"authenticated\",\"timestamp\":\"2024-01-01T00:00:00\"}")));
|
||||
|
||||
TestApi client = config.createClient(TestApi.class);
|
||||
TestResponse response = client.getTest();
|
||||
|
||||
assertThat(response).isNotNull();
|
||||
assertThat(response.getMessage()).isEqualTo("authenticated");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testNullValueHandling() {
|
||||
wireMockServer
|
||||
.stubFor(post(urlEqualTo("/test")).willReturn(aResponse().withStatus(200).withHeader("Content-Type", "application/json")
|
||||
.withBody("{\"id\":1,\"message\":null,\"timestamp\":\"2024-01-01T00:00:00\"}")));
|
||||
|
||||
TestApi client = config.createClient(TestApi.class);
|
||||
TestRequest request = new TestRequest();
|
||||
request.setName("null-test");
|
||||
request.setDate(null);
|
||||
|
||||
TestResponse response = client.createTest(request);
|
||||
|
||||
assertThat(response).isNotNull();
|
||||
assertThat(response.getMessage()).isNull();
|
||||
}
|
||||
|
||||
interface TestApi {
|
||||
|
||||
@RequestLine("GET /test")
|
||||
TestResponse getTest();
|
||||
|
||||
@RequestLine("POST /test")
|
||||
TestResponse createTest(TestRequest request);
|
||||
|
||||
@RequestLine("GET /error")
|
||||
TestResponse getError();
|
||||
}
|
||||
|
||||
static class TestRequest {
|
||||
|
||||
private String name;
|
||||
private LocalDate date;
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public LocalDate getDate() {
|
||||
return date;
|
||||
}
|
||||
|
||||
public void setDate(LocalDate date) {
|
||||
this.date = date;
|
||||
}
|
||||
}
|
||||
|
||||
static class TestResponse {
|
||||
|
||||
private Long id;
|
||||
private String message;
|
||||
private LocalDateTime timestamp;
|
||||
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(Long id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public String getMessage() {
|
||||
return message;
|
||||
}
|
||||
|
||||
public void setMessage(String message) {
|
||||
this.message = message;
|
||||
}
|
||||
|
||||
public LocalDateTime getTimestamp() {
|
||||
return timestamp;
|
||||
}
|
||||
|
||||
public void setTimestamp(LocalDateTime timestamp) {
|
||||
this.timestamp = timestamp;
|
||||
}
|
||||
}
|
||||
}
|
||||
+269
@@ -0,0 +1,269 @@
|
||||
/**
|
||||
* 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.performance;
|
||||
|
||||
import static com.github.tomakehurst.wiremock.client.WireMock.aResponse;
|
||||
import static com.github.tomakehurst.wiremock.client.WireMock.get;
|
||||
import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
import com.github.tomakehurst.wiremock.WireMockServer;
|
||||
import com.github.tomakehurst.wiremock.core.WireMockConfiguration;
|
||||
import feign.RequestLine;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.Future;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import org.apache.fineract.client.feign.FineractFeignClientConfig;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Tag;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
@Tag("performance")
|
||||
class ConnectionPoolPerformanceTest {
|
||||
|
||||
private WireMockServer wireMockServer;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
wireMockServer = new WireMockServer(WireMockConfiguration.wireMockConfig().dynamicPort());
|
||||
wireMockServer.start();
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
void tearDown() {
|
||||
if (wireMockServer != null) {
|
||||
wireMockServer.stop();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void testConnectionPoolTTL() throws Exception {
|
||||
FineractFeignClientConfig config = FineractFeignClientConfig.builder().baseUrl("http://localhost:" + wireMockServer.port())
|
||||
.credentials("test", "test").connectionTimeToLive(2, TimeUnit.SECONDS).connectTimeout(5, TimeUnit.SECONDS)
|
||||
.readTimeout(10, TimeUnit.SECONDS).build();
|
||||
|
||||
wireMockServer.stubFor(get(urlEqualTo("/test"))
|
||||
.willReturn(aResponse().withStatus(200).withHeader("Content-Type", "application/json").withBody("\"OK\"")));
|
||||
|
||||
TestApi api = config.createClient(TestApi.class);
|
||||
|
||||
String response1 = api.getTest();
|
||||
assertThat(response1).isEqualTo("OK");
|
||||
|
||||
Thread.sleep(3000);
|
||||
|
||||
String response2 = api.getTest();
|
||||
assertThat(response2).isEqualTo("OK");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testConnectionPoolTTLWithMultipleRequests() throws Exception {
|
||||
FineractFeignClientConfig config = FineractFeignClientConfig.builder().baseUrl("http://localhost:" + wireMockServer.port())
|
||||
.credentials("test", "test").connectionTimeToLive(1, TimeUnit.SECONDS).connectTimeout(5, TimeUnit.SECONDS)
|
||||
.readTimeout(10, TimeUnit.SECONDS).build();
|
||||
|
||||
wireMockServer.stubFor(get(urlEqualTo("/test"))
|
||||
.willReturn(aResponse().withStatus(200).withHeader("Content-Type", "application/json").withBody("\"OK\"")));
|
||||
|
||||
TestApi api = config.createClient(TestApi.class);
|
||||
|
||||
for (int i = 0; i < 5; i++) {
|
||||
String response = api.getTest();
|
||||
assertThat(response).isEqualTo("OK");
|
||||
Thread.sleep(1500);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void testConcurrentRequests() throws Exception {
|
||||
FineractFeignClientConfig config = FineractFeignClientConfig.builder().baseUrl("http://localhost:" + wireMockServer.port())
|
||||
.credentials("test", "test").connectTimeout(5, TimeUnit.SECONDS).readTimeout(10, TimeUnit.SECONDS).build();
|
||||
|
||||
wireMockServer.stubFor(get(urlEqualTo("/test")).willReturn(
|
||||
aResponse().withStatus(200).withHeader("Content-Type", "application/json").withBody("\"OK\"").withFixedDelay(100)));
|
||||
|
||||
TestApi api = config.createClient(TestApi.class);
|
||||
|
||||
int numRequests = 50;
|
||||
ExecutorService executor = Executors.newFixedThreadPool(10);
|
||||
List<Future<String>> futures = new ArrayList<>();
|
||||
|
||||
long startTime = System.currentTimeMillis();
|
||||
|
||||
for (int i = 0; i < numRequests; i++) {
|
||||
futures.add(executor.submit(api::getTest));
|
||||
}
|
||||
|
||||
for (Future<String> future : futures) {
|
||||
assertThat(future.get()).isEqualTo("OK");
|
||||
}
|
||||
|
||||
long duration = System.currentTimeMillis() - startTime;
|
||||
|
||||
executor.shutdown();
|
||||
executor.awaitTermination(30, TimeUnit.SECONDS);
|
||||
|
||||
assertThat(duration).isLessThan(2000);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testConcurrentRequestsWithHighLoad() throws Exception {
|
||||
FineractFeignClientConfig config = FineractFeignClientConfig.builder().baseUrl("http://localhost:" + wireMockServer.port())
|
||||
.credentials("test", "test").connectTimeout(5, TimeUnit.SECONDS).readTimeout(10, TimeUnit.SECONDS).build();
|
||||
|
||||
wireMockServer.stubFor(get(urlEqualTo("/test")).willReturn(
|
||||
aResponse().withStatus(200).withHeader("Content-Type", "application/json").withBody("\"OK\"").withFixedDelay(50)));
|
||||
|
||||
TestApi api = config.createClient(TestApi.class);
|
||||
|
||||
int numRequests = 100;
|
||||
ExecutorService executor = Executors.newFixedThreadPool(20);
|
||||
List<Future<String>> futures = new ArrayList<>();
|
||||
|
||||
long startTime = System.currentTimeMillis();
|
||||
|
||||
for (int i = 0; i < numRequests; i++) {
|
||||
futures.add(executor.submit(api::getTest));
|
||||
}
|
||||
|
||||
int successCount = 0;
|
||||
for (Future<String> future : futures) {
|
||||
if ("OK".equals(future.get())) {
|
||||
successCount++;
|
||||
}
|
||||
}
|
||||
|
||||
long duration = System.currentTimeMillis() - startTime;
|
||||
|
||||
executor.shutdown();
|
||||
executor.awaitTermination(30, TimeUnit.SECONDS);
|
||||
|
||||
assertThat(successCount).isEqualTo(numRequests);
|
||||
assertThat(duration).isLessThan(5000);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testConnectionRecycling() throws Exception {
|
||||
FineractFeignClientConfig config = FineractFeignClientConfig.builder().baseUrl("http://localhost:" + wireMockServer.port())
|
||||
.credentials("test", "test").connectionTimeToLive(500, TimeUnit.MILLISECONDS).connectTimeout(5, TimeUnit.SECONDS)
|
||||
.readTimeout(10, TimeUnit.SECONDS).build();
|
||||
|
||||
wireMockServer.stubFor(get(urlEqualTo("/test"))
|
||||
.willReturn(aResponse().withStatus(200).withHeader("Content-Type", "application/json").withBody("\"OK\"")));
|
||||
|
||||
TestApi api = config.createClient(TestApi.class);
|
||||
|
||||
for (int i = 0; i < 10; i++) {
|
||||
String response = api.getTest();
|
||||
assertThat(response).isEqualTo("OK");
|
||||
Thread.sleep(600);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void testDefaultConnectionTimeToLive() throws Exception {
|
||||
FineractFeignClientConfig config = FineractFeignClientConfig.builder().baseUrl("http://localhost:" + wireMockServer.port())
|
||||
.credentials("test", "test").build();
|
||||
|
||||
wireMockServer.stubFor(get(urlEqualTo("/test"))
|
||||
.willReturn(aResponse().withStatus(200).withHeader("Content-Type", "application/json").withBody("\"OK\"")));
|
||||
|
||||
TestApi api = config.createClient(TestApi.class);
|
||||
|
||||
String response = api.getTest();
|
||||
assertThat(response).isEqualTo("OK");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSequentialRequests() throws Exception {
|
||||
FineractFeignClientConfig config = FineractFeignClientConfig.builder().baseUrl("http://localhost:" + wireMockServer.port())
|
||||
.credentials("test", "test").connectTimeout(5, TimeUnit.SECONDS).readTimeout(10, TimeUnit.SECONDS).build();
|
||||
|
||||
wireMockServer.stubFor(get(urlEqualTo("/test"))
|
||||
.willReturn(aResponse().withStatus(200).withHeader("Content-Type", "application/json").withBody("\"OK\"")));
|
||||
|
||||
TestApi api = config.createClient(TestApi.class);
|
||||
|
||||
long startTime = System.currentTimeMillis();
|
||||
|
||||
for (int i = 0; i < 100; i++) {
|
||||
String response = api.getTest();
|
||||
assertThat(response).isEqualTo("OK");
|
||||
}
|
||||
|
||||
long duration = System.currentTimeMillis() - startTime;
|
||||
|
||||
assertThat(duration).isLessThan(5000);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testConnectionPoolWithDifferentEndpoints() throws Exception {
|
||||
FineractFeignClientConfig config = FineractFeignClientConfig.builder().baseUrl("http://localhost:" + wireMockServer.port())
|
||||
.credentials("test", "test").connectTimeout(5, TimeUnit.SECONDS).readTimeout(10, TimeUnit.SECONDS).build();
|
||||
|
||||
wireMockServer.stubFor(get(urlEqualTo("/test"))
|
||||
.willReturn(aResponse().withStatus(200).withHeader("Content-Type", "application/json").withBody("\"OK\"")));
|
||||
wireMockServer.stubFor(get(urlEqualTo("/test2"))
|
||||
.willReturn(aResponse().withStatus(200).withHeader("Content-Type", "application/json").withBody("\"OK2\"")));
|
||||
wireMockServer.stubFor(get(urlEqualTo("/test3"))
|
||||
.willReturn(aResponse().withStatus(200).withHeader("Content-Type", "application/json").withBody("\"OK3\"")));
|
||||
|
||||
TestApi api = config.createClient(TestApi.class);
|
||||
|
||||
ExecutorService executor = Executors.newFixedThreadPool(5);
|
||||
List<Future<String>> futures = new ArrayList<>();
|
||||
|
||||
for (int i = 0; i < 30; i++) {
|
||||
int endpoint = i % 3;
|
||||
futures.add(executor.submit(() -> {
|
||||
switch (endpoint) {
|
||||
case 0:
|
||||
return api.getTest();
|
||||
case 1:
|
||||
return api.getTest2();
|
||||
default:
|
||||
return api.getTest3();
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
for (Future<String> future : futures) {
|
||||
assertThat(future.get()).isIn("OK", "OK2", "OK3");
|
||||
}
|
||||
|
||||
executor.shutdown();
|
||||
executor.awaitTermination(30, TimeUnit.SECONDS);
|
||||
}
|
||||
|
||||
interface TestApi {
|
||||
|
||||
@RequestLine("GET /test")
|
||||
String getTest();
|
||||
|
||||
@RequestLine("GET /test2")
|
||||
String getTest2();
|
||||
|
||||
@RequestLine("GET /test3")
|
||||
String getTest3();
|
||||
}
|
||||
}
|
||||
+208
@@ -0,0 +1,208 @@
|
||||
/**
|
||||
* 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.services;
|
||||
|
||||
import static com.github.tomakehurst.wiremock.client.WireMock.aResponse;
|
||||
import static com.github.tomakehurst.wiremock.client.WireMock.delete;
|
||||
import static com.github.tomakehurst.wiremock.client.WireMock.get;
|
||||
import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
import com.github.tomakehurst.wiremock.WireMockServer;
|
||||
import com.github.tomakehurst.wiremock.core.WireMockConfiguration;
|
||||
import feign.Response;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import org.apache.fineract.client.feign.FineractFeignClientConfig;
|
||||
import org.apache.fineract.client.feign.services.DocumentsApiFixed;
|
||||
import org.apache.fineract.client.models.DeleteEntityTypeEntityIdDocumentsResponse;
|
||||
import org.apache.fineract.client.models.DocumentData;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Tag;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
|
||||
@Tag("integration")
|
||||
class DocumentsApiFixedIntegrationTest {
|
||||
|
||||
@TempDir
|
||||
File tempDir;
|
||||
|
||||
private WireMockServer wireMockServer;
|
||||
private FineractFeignClientConfig config;
|
||||
private File testDocumentFile;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() throws IOException {
|
||||
wireMockServer = new WireMockServer(WireMockConfiguration.wireMockConfig().dynamicPort());
|
||||
wireMockServer.start();
|
||||
config = FineractFeignClientConfig.builder().baseUrl("http://localhost:" + wireMockServer.port()).credentials("test", "test")
|
||||
.connectTimeout(5, TimeUnit.SECONDS).readTimeout(10, TimeUnit.SECONDS).build();
|
||||
|
||||
testDocumentFile = new File(tempDir, "test-doc.pdf");
|
||||
Files.write(testDocumentFile.toPath(), "%PDF-1.4 test content".getBytes(StandardCharsets.UTF_8));
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
void tearDown() {
|
||||
if (wireMockServer != null) {
|
||||
wireMockServer.stop();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetDocumentsForClient() {
|
||||
String responseBody = "[{\"id\":1,\"name\":\"Document1\",\"description\":\"Test document\"}]";
|
||||
wireMockServer.stubFor(get(urlEqualTo("/v1/clients/123/documents"))
|
||||
.willReturn(aResponse().withStatus(200).withHeader("Content-Type", "application/json").withBody(responseBody)));
|
||||
|
||||
DocumentsApiFixed api = config.createClient(DocumentsApiFixed.class);
|
||||
List<DocumentData> documents = api.retrieveAllDocuments("clients", 123L);
|
||||
|
||||
assertThat(documents).isNotNull();
|
||||
assertThat(documents).hasSize(1);
|
||||
assertThat(documents.get(0).getId()).isEqualTo(1L);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetDocumentsForLoan() {
|
||||
String responseBody = "[{\"id\":2,\"name\":\"LoanDocument\",\"description\":\"Loan agreement\"}]";
|
||||
wireMockServer.stubFor(get(urlEqualTo("/v1/loans/456/documents"))
|
||||
.willReturn(aResponse().withStatus(200).withHeader("Content-Type", "application/json").withBody(responseBody)));
|
||||
|
||||
DocumentsApiFixed api = config.createClient(DocumentsApiFixed.class);
|
||||
List<DocumentData> documents = api.retrieveAllDocuments("loans", 456L);
|
||||
|
||||
assertThat(documents).isNotNull();
|
||||
assertThat(documents).hasSize(1);
|
||||
assertThat(documents.get(0).getName()).isEqualTo("LoanDocument");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetDocumentsEmpty() {
|
||||
wireMockServer.stubFor(get(urlEqualTo("/v1/clients/999/documents"))
|
||||
.willReturn(aResponse().withStatus(200).withHeader("Content-Type", "application/json").withBody("[]")));
|
||||
|
||||
DocumentsApiFixed api = config.createClient(DocumentsApiFixed.class);
|
||||
List<DocumentData> documents = api.retrieveAllDocuments("clients", 999L);
|
||||
|
||||
assertThat(documents).isNotNull();
|
||||
assertThat(documents).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetSingleDocument() {
|
||||
String responseBody = "{\"id\":1,\"name\":\"Document1\",\"description\":\"Test document\",\"fileName\":\"doc.pdf\"}";
|
||||
wireMockServer.stubFor(get(urlEqualTo("/v1/clients/123/documents/1"))
|
||||
.willReturn(aResponse().withStatus(200).withHeader("Content-Type", "application/json").withBody(responseBody)));
|
||||
|
||||
DocumentsApiFixed api = config.createClient(DocumentsApiFixed.class);
|
||||
DocumentData document = api.getDocument("clients", 123L, 1L);
|
||||
|
||||
assertThat(document).isNotNull();
|
||||
assertThat(document.getId()).isEqualTo(1L);
|
||||
assertThat(document.getName()).isEqualTo("Document1");
|
||||
assertThat(document.getFileName()).isEqualTo("doc.pdf");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testDownloadDocument() throws IOException {
|
||||
byte[] docContent = "PDF document content here".getBytes(StandardCharsets.UTF_8);
|
||||
wireMockServer.stubFor(get(urlEqualTo("/v1/clients/123/documents/456/attachment"))
|
||||
.willReturn(aResponse().withStatus(200).withHeader("Content-Type", "application/pdf")
|
||||
.withHeader("Content-Disposition", "attachment; filename=\"document.pdf\"").withBody(docContent)));
|
||||
|
||||
DocumentsApiFixed api = config.createClient(DocumentsApiFixed.class);
|
||||
Response response = api.downloadFile("clients", 123L, 456L);
|
||||
|
||||
assertThat(response.status()).isEqualTo(200);
|
||||
assertThat(response.headers()).containsKey("Content-Type");
|
||||
assertThat(response.headers().get("Content-Type")).contains("application/pdf");
|
||||
assertThat(response.body().asInputStream().readAllBytes()).isEqualTo(docContent);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testDeleteDocument() {
|
||||
String responseBody = "{\"resourceId\":456}";
|
||||
wireMockServer.stubFor(delete(urlEqualTo("/v1/clients/123/documents/456"))
|
||||
.willReturn(aResponse().withStatus(200).withHeader("Content-Type", "application/json").withBody(responseBody)));
|
||||
|
||||
DocumentsApiFixed api = config.createClient(DocumentsApiFixed.class);
|
||||
DeleteEntityTypeEntityIdDocumentsResponse response = api.deleteDocument("clients", 123L, 456L);
|
||||
|
||||
assertThat(response).isNotNull();
|
||||
assertThat(response.getResourceId()).isEqualTo(456L);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testDeleteDocumentForLoan() {
|
||||
String responseBody = "{\"resourceId\":999}";
|
||||
wireMockServer.stubFor(delete(urlEqualTo("/v1/loans/789/documents/999"))
|
||||
.willReturn(aResponse().withStatus(200).withHeader("Content-Type", "application/json").withBody(responseBody)));
|
||||
|
||||
DocumentsApiFixed api = config.createClient(DocumentsApiFixed.class);
|
||||
DeleteEntityTypeEntityIdDocumentsResponse response = api.deleteDocument("loans", 789L, 999L);
|
||||
|
||||
assertThat(response).isNotNull();
|
||||
assertThat(response.getResourceId()).isEqualTo(999L);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testMultipleEntityTypes() {
|
||||
String[] entityTypes = { "clients", "loans", "savings", "groups" };
|
||||
Long[] entityIds = { 100L, 200L, 300L, 400L };
|
||||
|
||||
for (int i = 0; i < entityTypes.length; i++) {
|
||||
String entityType = entityTypes[i];
|
||||
Long entityId = entityIds[i];
|
||||
|
||||
wireMockServer.stubFor(get(urlEqualTo("/v1/" + entityType + "/" + entityId + "/documents"))
|
||||
.willReturn(aResponse().withStatus(200).withHeader("Content-Type", "application/json").withBody("[]")));
|
||||
}
|
||||
|
||||
DocumentsApiFixed api = config.createClient(DocumentsApiFixed.class);
|
||||
|
||||
for (int i = 0; i < entityTypes.length; i++) {
|
||||
List<DocumentData> documents = api.retrieveAllDocuments(entityTypes[i], entityIds[i]);
|
||||
assertThat(documents).isNotNull();
|
||||
assertThat(documents).isEmpty();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void testDownloadTextDocument() throws IOException {
|
||||
String textContent = "Plain text document content";
|
||||
wireMockServer.stubFor(get(urlEqualTo("/v1/clients/123/documents/999/attachment"))
|
||||
.willReturn(aResponse().withStatus(200).withHeader("Content-Type", "text/plain")
|
||||
.withHeader("Content-Disposition", "attachment; filename=\"readme.txt\"").withBody(textContent)));
|
||||
|
||||
DocumentsApiFixed api = config.createClient(DocumentsApiFixed.class);
|
||||
Response response = api.downloadFile("clients", 123L, 999L);
|
||||
|
||||
assertThat(response.status()).isEqualTo(200);
|
||||
assertThat(response.headers().get("Content-Type")).contains("text/plain");
|
||||
String content = new String(response.body().asInputStream().readAllBytes(), StandardCharsets.UTF_8);
|
||||
assertThat(content).isEqualTo(textContent);
|
||||
}
|
||||
}
|
||||
+180
@@ -0,0 +1,180 @@
|
||||
/**
|
||||
* 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.services;
|
||||
|
||||
import static com.github.tomakehurst.wiremock.client.WireMock.aResponse;
|
||||
import static com.github.tomakehurst.wiremock.client.WireMock.delete;
|
||||
import static com.github.tomakehurst.wiremock.client.WireMock.equalTo;
|
||||
import static com.github.tomakehurst.wiremock.client.WireMock.get;
|
||||
import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo;
|
||||
import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
import com.github.tomakehurst.wiremock.WireMockServer;
|
||||
import com.github.tomakehurst.wiremock.core.WireMockConfiguration;
|
||||
import feign.Response;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import org.apache.fineract.client.feign.FineractFeignClientConfig;
|
||||
import org.apache.fineract.client.feign.services.ImagesApi;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Tag;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
|
||||
@Tag("integration")
|
||||
class ImagesApiIntegrationTest {
|
||||
|
||||
@TempDir
|
||||
File tempDir;
|
||||
|
||||
private WireMockServer wireMockServer;
|
||||
private FineractFeignClientConfig config;
|
||||
private File testImageFile;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() throws IOException {
|
||||
wireMockServer = new WireMockServer(WireMockConfiguration.wireMockConfig().dynamicPort());
|
||||
wireMockServer.start();
|
||||
config = FineractFeignClientConfig.builder().baseUrl("http://localhost:" + wireMockServer.port()).credentials("test", "test")
|
||||
.connectTimeout(5, TimeUnit.SECONDS).readTimeout(10, TimeUnit.SECONDS).build();
|
||||
|
||||
testImageFile = new File(tempDir, "test-image.jpg");
|
||||
Files.write(testImageFile.toPath(), new byte[] { (byte) 0xFF, (byte) 0xD8, (byte) 0xFF, 0x01, 0x02, 0x03 });
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
void tearDown() {
|
||||
if (wireMockServer != null) {
|
||||
wireMockServer.stop();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void testRetrieveClientImage() throws IOException {
|
||||
byte[] imageData = new byte[] { (byte) 0xFF, (byte) 0xD8, (byte) 0xFF, 0x10, 0x11, 0x12 };
|
||||
wireMockServer.stubFor(get(urlEqualTo("/v1/clients/123/images"))
|
||||
.willReturn(aResponse().withStatus(200).withHeader("Content-Type", "image/jpeg").withBody(imageData)));
|
||||
|
||||
ImagesApi api = config.createClient(ImagesApi.class);
|
||||
Response response = api.get("clients", 123L, new HashMap<>());
|
||||
|
||||
assertThat(response.status()).isEqualTo(200);
|
||||
assertThat(response.headers()).containsKey("Content-Type");
|
||||
assertThat(response.headers().get("Content-Type")).contains("image/jpeg");
|
||||
assertThat(response.body().asInputStream().readAllBytes()).isEqualTo(imageData);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testRetrieveClientImageWithMaxDimensions() throws IOException {
|
||||
byte[] resizedImage = new byte[] { (byte) 0xFF, (byte) 0xD8, (byte) 0xFF, 0x20, 0x21 };
|
||||
wireMockServer.stubFor(get(urlPathEqualTo("/v1/clients/123/images")).withQueryParam("maxWidth", equalTo("100"))
|
||||
.withQueryParam("maxHeight", equalTo("100"))
|
||||
.willReturn(aResponse().withStatus(200).withHeader("Content-Type", "image/jpeg").withBody(resizedImage)));
|
||||
|
||||
ImagesApi api = config.createClient(ImagesApi.class);
|
||||
Map<String, Object> params = new HashMap<>();
|
||||
params.put("maxWidth", 100);
|
||||
params.put("maxHeight", 100);
|
||||
|
||||
Response response = api.get("clients", 123L, params);
|
||||
|
||||
assertThat(response.status()).isEqualTo(200);
|
||||
assertThat(response.body().asInputStream().readAllBytes()).isEqualTo(resizedImage);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testRetrieveClientImageWithMaxWidthOnly() throws IOException {
|
||||
byte[] resizedImage = new byte[] { (byte) 0xFF, (byte) 0xD8, (byte) 0xFF, 0x30 };
|
||||
wireMockServer.stubFor(get(urlPathEqualTo("/v1/clients/123/images")).withQueryParam("maxWidth", equalTo("200"))
|
||||
.willReturn(aResponse().withStatus(200).withHeader("Content-Type", "image/jpeg").withBody(resizedImage)));
|
||||
|
||||
ImagesApi api = config.createClient(ImagesApi.class);
|
||||
Map<String, Object> params = new HashMap<>();
|
||||
params.put("maxWidth", 200);
|
||||
|
||||
Response response = api.get("clients", 123L, params);
|
||||
|
||||
assertThat(response.status()).isEqualTo(200);
|
||||
assertThat(response.body().asInputStream().readAllBytes()).isEqualTo(resizedImage);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testDeleteClientImage() {
|
||||
wireMockServer.stubFor(delete(urlEqualTo("/v1/clients/123/images"))
|
||||
.willReturn(aResponse().withStatus(200).withHeader("Content-Type", "application/json").withBody("{\"resourceId\":123}")));
|
||||
|
||||
ImagesApi api = config.createClient(ImagesApi.class);
|
||||
Response response = api.delete("clients", 123L);
|
||||
|
||||
assertThat(response.status()).isEqualTo(200);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testDeleteStaffImage() {
|
||||
wireMockServer.stubFor(delete(urlEqualTo("/v1/staff/456/images"))
|
||||
.willReturn(aResponse().withStatus(200).withHeader("Content-Type", "application/json").withBody("{\"resourceId\":456}")));
|
||||
|
||||
ImagesApi api = config.createClient(ImagesApi.class);
|
||||
Response response = api.delete("staff", 456L);
|
||||
|
||||
assertThat(response.status()).isEqualTo(200);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testRetrieveImageWithPngFormat() throws IOException {
|
||||
byte[] pngImage = new byte[] { (byte) 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A };
|
||||
wireMockServer.stubFor(get(urlEqualTo("/v1/clients/123/images"))
|
||||
.willReturn(aResponse().withStatus(200).withHeader("Content-Type", "image/png").withBody(pngImage)));
|
||||
|
||||
ImagesApi api = config.createClient(ImagesApi.class);
|
||||
Response response = api.get("clients", 123L, new HashMap<>());
|
||||
|
||||
assertThat(response.status()).isEqualTo(200);
|
||||
assertThat(response.headers().get("Content-Type")).contains("image/png");
|
||||
assertThat(response.body().asInputStream().readAllBytes()).isEqualTo(pngImage);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testMultipleEntityTypes() {
|
||||
String[] entityTypes = { "clients", "staff" };
|
||||
Long[] entityIds = { 100L, 200L };
|
||||
|
||||
for (int i = 0; i < entityTypes.length; i++) {
|
||||
String entityType = entityTypes[i];
|
||||
Long entityId = entityIds[i];
|
||||
|
||||
wireMockServer.stubFor(get(urlEqualTo("/v1/" + entityType + "/" + entityId + "/images"))
|
||||
.willReturn(aResponse().withStatus(200).withHeader("Content-Type", "image/jpeg").withBody(new byte[] { 0x01 })));
|
||||
}
|
||||
|
||||
ImagesApi api = config.createClient(ImagesApi.class);
|
||||
|
||||
for (int i = 0; i < entityTypes.length; i++) {
|
||||
Response response = api.get(entityTypes[i], entityIds[i], new HashMap<>());
|
||||
assertThat(response.status()).isEqualTo(200);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+166
@@ -0,0 +1,166 @@
|
||||
/**
|
||||
* 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.services;
|
||||
|
||||
import static com.github.tomakehurst.wiremock.client.WireMock.aResponse;
|
||||
import static com.github.tomakehurst.wiremock.client.WireMock.equalTo;
|
||||
import static com.github.tomakehurst.wiremock.client.WireMock.get;
|
||||
import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
import com.github.tomakehurst.wiremock.WireMockServer;
|
||||
import com.github.tomakehurst.wiremock.core.WireMockConfiguration;
|
||||
import feign.Response;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import org.apache.fineract.client.feign.FineractFeignClientConfig;
|
||||
import org.apache.fineract.client.feign.services.RunReportsApi;
|
||||
import org.apache.fineract.client.models.RunReportsResponse;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Tag;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
@Tag("integration")
|
||||
class RunReportsApiIntegrationTest {
|
||||
|
||||
private WireMockServer wireMockServer;
|
||||
private FineractFeignClientConfig config;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
wireMockServer = new WireMockServer(WireMockConfiguration.wireMockConfig().dynamicPort());
|
||||
wireMockServer.start();
|
||||
config = FineractFeignClientConfig.builder().baseUrl("http://localhost:" + wireMockServer.port()).credentials("test", "test")
|
||||
.connectTimeout(5, TimeUnit.SECONDS).readTimeout(10, TimeUnit.SECONDS).build();
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
void tearDown() {
|
||||
if (wireMockServer != null) {
|
||||
wireMockServer.stop();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void testRunReportGetDataWithMultipleParameters() {
|
||||
wireMockServer.stubFor(get(urlPathEqualTo("/v1/runreports/DateRangeReport")).withQueryParam("officeId", equalTo("1"))
|
||||
.withQueryParam("fromDate", equalTo("2024-01-01")).withQueryParam("toDate", equalTo("2024-12-31")).willReturn(aResponse()
|
||||
.withStatus(200).withHeader("Content-Type", "application/json").withBody("{\"columnHeaders\":[],\"data\":[]}")));
|
||||
|
||||
RunReportsApi api = config.createClient(RunReportsApi.class);
|
||||
Map<String, String> params = new HashMap<>();
|
||||
params.put("officeId", "1");
|
||||
params.put("fromDate", "2024-01-01");
|
||||
params.put("toDate", "2024-12-31");
|
||||
|
||||
RunReportsResponse response = api.runReportGetData("DateRangeReport", params);
|
||||
|
||||
assertThat(response).isNotNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testRunReportGetDataWithEmptyResult() {
|
||||
wireMockServer.stubFor(get(urlPathEqualTo("/v1/runreports/EmptyReport")).withQueryParam("officeId", equalTo("1")).willReturn(
|
||||
aResponse().withStatus(200).withHeader("Content-Type", "application/json").withBody("{\"columnHeaders\":[],\"data\":[]}")));
|
||||
|
||||
RunReportsApi api = config.createClient(RunReportsApi.class);
|
||||
Map<String, String> params = new HashMap<>();
|
||||
params.put("officeId", "1");
|
||||
|
||||
RunReportsResponse response = api.runReportGetData("EmptyReport", params);
|
||||
|
||||
assertThat(response).isNotNull();
|
||||
assertThat(response.getColumnHeaders()).isEmpty();
|
||||
assertThat(response.getData()).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testRunReportGetFileAsPdf() throws IOException {
|
||||
byte[] pdfContent = "%PDF-1.4 test content".getBytes(StandardCharsets.UTF_8);
|
||||
wireMockServer.stubFor(get(urlPathEqualTo("/v1/runreports/LoanSummary")).withQueryParam("output-type", equalTo("PDF"))
|
||||
.willReturn(aResponse().withStatus(200).withHeader("Content-Type", "application/pdf")
|
||||
.withHeader("Content-Disposition", "attachment; filename=\"report.pdf\"").withBody(pdfContent)));
|
||||
|
||||
RunReportsApi api = config.createClient(RunReportsApi.class);
|
||||
Map<String, String> params = new HashMap<>();
|
||||
params.put("output-type", "PDF");
|
||||
|
||||
Response response = api.runReportGetFile("LoanSummary", params);
|
||||
|
||||
assertThat(response.status()).isEqualTo(200);
|
||||
assertThat(response.headers()).containsKey("Content-Type");
|
||||
assertThat(response.headers().get("Content-Type")).contains("application/pdf");
|
||||
assertThat(response.body().asInputStream().readAllBytes()).isEqualTo(pdfContent);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testRunReportGetFileAsCsv() throws IOException {
|
||||
String csvContent = "id,name,balance\n1,Client1,1000.00\n2,Client2,2000.00";
|
||||
wireMockServer.stubFor(get(urlPathEqualTo("/v1/runreports/ClientBalance")).withQueryParam("exportCSV", equalTo("true"))
|
||||
.willReturn(aResponse().withStatus(200).withHeader("Content-Type", "text/csv")
|
||||
.withHeader("Content-Disposition", "attachment; filename=\"report.csv\"").withBody(csvContent)));
|
||||
|
||||
RunReportsApi api = config.createClient(RunReportsApi.class);
|
||||
Map<String, String> params = new HashMap<>();
|
||||
params.put("exportCSV", "true");
|
||||
|
||||
Response response = api.runReportGetFile("ClientBalance", params);
|
||||
|
||||
assertThat(response.status()).isEqualTo(200);
|
||||
assertThat(response.headers()).containsKey("Content-Type");
|
||||
assertThat(response.headers().get("Content-Type")).contains("text/csv");
|
||||
String content = new String(response.body().asInputStream().readAllBytes(), StandardCharsets.UTF_8);
|
||||
assertThat(content).isEqualTo(csvContent);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testRunReportGetFileAsExcel() throws IOException {
|
||||
byte[] excelContent = "fake excel data".getBytes(StandardCharsets.UTF_8);
|
||||
wireMockServer.stubFor(get(urlPathEqualTo("/v1/runreports/FinancialReport")).withQueryParam("output-type", equalTo("XLS"))
|
||||
.willReturn(aResponse().withStatus(200).withHeader("Content-Type", "application/vnd.ms-excel")
|
||||
.withHeader("Content-Disposition", "attachment; filename=\"report.xls\"").withBody(excelContent)));
|
||||
|
||||
RunReportsApi api = config.createClient(RunReportsApi.class);
|
||||
Map<String, String> params = new HashMap<>();
|
||||
params.put("output-type", "XLS");
|
||||
|
||||
Response response = api.runReportGetFile("FinancialReport", params);
|
||||
|
||||
assertThat(response.status()).isEqualTo(200);
|
||||
assertThat(response.headers()).containsKey("Content-Type");
|
||||
assertThat(response.body().asInputStream().readAllBytes()).isEqualTo(excelContent);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testRunReportGetDataWithNoParameters() {
|
||||
wireMockServer.stubFor(get(urlPathEqualTo("/v1/runreports/AllClients")).willReturn(
|
||||
aResponse().withStatus(200).withHeader("Content-Type", "application/json").withBody("{\"columnHeaders\":[],\"data\":[]}")));
|
||||
|
||||
RunReportsApi api = config.createClient(RunReportsApi.class);
|
||||
|
||||
RunReportsResponse response = api.runReportGetData("AllClients", new HashMap<>());
|
||||
|
||||
assertThat(response).isNotNull();
|
||||
}
|
||||
|
||||
}
|
||||
+296
@@ -0,0 +1,296 @@
|
||||
/**
|
||||
* 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 static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
import feign.Request;
|
||||
import feign.Response;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
|
||||
class FeignPartsTest {
|
||||
|
||||
@TempDir
|
||||
File tempDir;
|
||||
|
||||
@Test
|
||||
void testMediaTypeForJpeg() {
|
||||
String mediaType = FeignParts.mediaType("photo.jpg");
|
||||
|
||||
assertThat(mediaType).isEqualTo("image/jpeg");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testMediaTypeForJpegUppercase() {
|
||||
String mediaType = FeignParts.mediaType("photo.JPEG");
|
||||
|
||||
assertThat(mediaType).isEqualTo("image/jpeg");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testMediaTypeForPng() {
|
||||
String mediaType = FeignParts.mediaType("image.png");
|
||||
|
||||
assertThat(mediaType).isEqualTo("image/png");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testMediaTypeForPdf() {
|
||||
String mediaType = FeignParts.mediaType("document.pdf");
|
||||
|
||||
assertThat(mediaType).isEqualTo("application/pdf");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testMediaTypeForDocx() {
|
||||
String mediaType = FeignParts.mediaType("report.docx");
|
||||
|
||||
assertThat(mediaType).isEqualTo("application/vnd.openxmlformats-officedocument.wordprocessingml.document");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testMediaTypeForDoc() {
|
||||
String mediaType = FeignParts.mediaType("report.doc");
|
||||
|
||||
assertThat(mediaType).isEqualTo("application/msword");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testMediaTypeForXlsx() {
|
||||
String mediaType = FeignParts.mediaType("spreadsheet.xlsx");
|
||||
|
||||
assertThat(mediaType).isEqualTo("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testMediaTypeForXls() {
|
||||
String mediaType = FeignParts.mediaType("spreadsheet.xls");
|
||||
|
||||
assertThat(mediaType).isEqualTo("application/vnd.ms-excel");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testMediaTypeForTiff() {
|
||||
String mediaType = FeignParts.mediaType("scan.tiff");
|
||||
|
||||
assertThat(mediaType).isEqualTo("image/tiff");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testMediaTypeForGif() {
|
||||
String mediaType = FeignParts.mediaType("animation.gif");
|
||||
|
||||
assertThat(mediaType).isEqualTo("image/gif");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testMediaTypeForOdt() {
|
||||
String mediaType = FeignParts.mediaType("document.odt");
|
||||
|
||||
assertThat(mediaType).isEqualTo("application/vnd.oasis.opendocument.text");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testMediaTypeForOds() {
|
||||
String mediaType = FeignParts.mediaType("spreadsheet.ods");
|
||||
|
||||
assertThat(mediaType).isEqualTo("application/vnd.oasis.opendocument.spreadsheet");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testMediaTypeForTxt() {
|
||||
String mediaType = FeignParts.mediaType("readme.txt");
|
||||
|
||||
assertThat(mediaType).isEqualTo("text/plain");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testMediaTypeForUnknownExtension() {
|
||||
String mediaType = FeignParts.mediaType("file.xyz");
|
||||
|
||||
assertThat(mediaType).isEqualTo("application/octet-stream");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testMediaTypeCaseInsensitive() {
|
||||
assertThat(FeignParts.mediaType("FILE.JPG")).isEqualTo("image/jpeg");
|
||||
assertThat(FeignParts.mediaType("file.PDF")).isEqualTo("application/pdf");
|
||||
assertThat(FeignParts.mediaType("Document.DOCX"))
|
||||
.isEqualTo("application/vnd.openxmlformats-officedocument.wordprocessingml.document");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testMediaTypeForNullFilename() {
|
||||
String mediaType = FeignParts.mediaType(null);
|
||||
|
||||
assertThat(mediaType).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testMediaTypeForFilenameWithoutExtension() {
|
||||
String mediaType = FeignParts.mediaType("filename");
|
||||
|
||||
assertThat(mediaType).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFileNameExtractionFromContentDisposition() {
|
||||
Response response = Response.builder().status(200)
|
||||
.request(Request.create(Request.HttpMethod.GET, "/test", Map.of(), null, null, null))
|
||||
.headers(Map.of("Content-Disposition", List.of("attachment; filename=\"test.pdf\""))).body(new byte[0]).build();
|
||||
|
||||
Optional<String> filename = FeignParts.fileName(response);
|
||||
|
||||
assertThat(filename).isPresent();
|
||||
assertThat(filename.get()).isEqualTo("test.pdf");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFileNameExtractionWithComplexFilename() {
|
||||
Response response = Response.builder().status(200)
|
||||
.request(Request.create(Request.HttpMethod.GET, "/test", Map.of(), null, null, null))
|
||||
.headers(Map.of("Content-Disposition", List.of("attachment; filename=\"report-2024-01-15.xlsx\""))).body(new byte[0])
|
||||
.build();
|
||||
|
||||
Optional<String> filename = FeignParts.fileName(response);
|
||||
|
||||
assertThat(filename).isPresent();
|
||||
assertThat(filename.get()).isEqualTo("report-2024-01-15.xlsx");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFileNameWithoutContentDisposition() {
|
||||
Response response = Response.builder().status(200)
|
||||
.request(Request.create(Request.HttpMethod.GET, "/test", Map.of(), null, null, null)).headers(Map.of()).body(new byte[0])
|
||||
.build();
|
||||
|
||||
Optional<String> filename = FeignParts.fileName(response);
|
||||
|
||||
assertThat(filename).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFileNameWithNullHeaders() {
|
||||
Response response = Response.builder().status(200)
|
||||
.request(Request.create(Request.HttpMethod.GET, "/test", Map.of(), null, null, null)).headers(null).body(new byte[0])
|
||||
.build();
|
||||
|
||||
Optional<String> filename = FeignParts.fileName(response);
|
||||
|
||||
assertThat(filename).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFileNameWithEmptyContentDisposition() {
|
||||
Response response = Response.builder().status(200)
|
||||
.request(Request.create(Request.HttpMethod.GET, "/test", Map.of(), null, null, null))
|
||||
.headers(Map.of("Content-Disposition", Collections.emptyList())).body(new byte[0]).build();
|
||||
|
||||
Optional<String> filename = FeignParts.fileName(response);
|
||||
|
||||
assertThat(filename).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFileNameWithInvalidContentDisposition() {
|
||||
Response response = Response.builder().status(200)
|
||||
.request(Request.create(Request.HttpMethod.GET, "/test", Map.of(), null, null, null))
|
||||
.headers(Map.of("Content-Disposition", List.of("inline"))).body(new byte[0]).build();
|
||||
|
||||
Optional<String> filename = FeignParts.fileName(response);
|
||||
|
||||
assertThat(filename).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testProbeContentTypeForJpegFile() throws IOException {
|
||||
File jpegFile = new File(tempDir, "test.jpg");
|
||||
Files.write(jpegFile.toPath(), new byte[] { (byte) 0xFF, (byte) 0xD8, (byte) 0xFF });
|
||||
|
||||
String contentType = FeignParts.probeContentType(jpegFile);
|
||||
|
||||
assertThat(contentType).isIn("image/jpeg", "image/jpg");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testProbeContentTypeForPdfFile() throws IOException {
|
||||
File pdfFile = new File(tempDir, "test.pdf");
|
||||
Files.write(pdfFile.toPath(), "%PDF-1.4".getBytes(StandardCharsets.UTF_8));
|
||||
|
||||
String contentType = FeignParts.probeContentType(pdfFile);
|
||||
|
||||
assertThat(contentType).isEqualTo("application/pdf");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testProbeContentTypeFallsBackToMediaType() throws IOException {
|
||||
File unknownFile = new File(tempDir, "test.docx");
|
||||
Files.write(unknownFile.toPath(), "test content".getBytes(StandardCharsets.UTF_8));
|
||||
|
||||
String contentType = FeignParts.probeContentType(unknownFile);
|
||||
|
||||
assertThat(contentType).isNotNull();
|
||||
assertThat(contentType).isNotEqualTo("application/octet-stream");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testProbeContentTypeForUnknownFile() throws IOException {
|
||||
File unknownFile = new File(tempDir, "test.xyz");
|
||||
Files.write(unknownFile.toPath(), "unknown content".getBytes(StandardCharsets.UTF_8));
|
||||
|
||||
String contentType = FeignParts.probeContentType(unknownFile);
|
||||
|
||||
assertThat(contentType).isNotNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testMediaTypeForAllSupportedImageFormats() {
|
||||
assertThat(FeignParts.mediaType("image.jpg")).isEqualTo("image/jpeg");
|
||||
assertThat(FeignParts.mediaType("image.jpeg")).isEqualTo("image/jpeg");
|
||||
assertThat(FeignParts.mediaType("image.png")).isEqualTo("image/png");
|
||||
assertThat(FeignParts.mediaType("image.gif")).isEqualTo("image/gif");
|
||||
assertThat(FeignParts.mediaType("image.tif")).isEqualTo("image/tiff");
|
||||
assertThat(FeignParts.mediaType("image.tiff")).isEqualTo("image/tiff");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testMediaTypeForAllSupportedDocumentFormats() {
|
||||
assertThat(FeignParts.mediaType("doc.pdf")).isEqualTo("application/pdf");
|
||||
assertThat(FeignParts.mediaType("doc.doc")).isEqualTo("application/msword");
|
||||
assertThat(FeignParts.mediaType("doc.docx")).isEqualTo("application/vnd.openxmlformats-officedocument.wordprocessingml.document");
|
||||
assertThat(FeignParts.mediaType("doc.odt")).isEqualTo("application/vnd.oasis.opendocument.text");
|
||||
assertThat(FeignParts.mediaType("doc.txt")).isEqualTo("text/plain");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testMediaTypeForAllSupportedSpreadsheetFormats() {
|
||||
assertThat(FeignParts.mediaType("sheet.xls")).isEqualTo("application/vnd.ms-excel");
|
||||
assertThat(FeignParts.mediaType("sheet.xlsx")).isEqualTo("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
|
||||
assertThat(FeignParts.mediaType("sheet.ods")).isEqualTo("application/vnd.oasis.opendocument.spreadsheet");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user