Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ repositories {

dependencies {
implementation("com.fasterxml.jackson.datatype:jackson-datatype-jsr310:${jacksonVersion}")
implementation("com.fasterxml.jackson.dataformat:jackson-dataformat-csv:${jacksonCsvVersion}")
implementation("com.fasterxml.jackson.core:jackson-databind:${jacksonVersion}")
implementation("org.apache.httpcomponents.client5:httpclient5:${apacheHttpComponentsVersion}")
implementation("org.apache.commons:commons-lang3:${apacheCommonsVersion}")
Expand Down
1 change: 1 addition & 0 deletions gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ checkstyleVersion=10.26.1
apacheCommonsVersion=3.18.0
apacheHttpComponentsVersion=5.5
jacksonVersion=2.19.2
jacksonCsvVersion=2.17.2

# Publishing
mavenCentralPublishing=true
Expand Down
9 changes: 9 additions & 0 deletions src/main/java/dev/sorn/fmp4j/FmpClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

import dev.sorn.fmp4j.cfg.FmpConfig;
import dev.sorn.fmp4j.cfg.FmpConfigImpl;
import dev.sorn.fmp4j.clients.FmpBulkClient;
import dev.sorn.fmp4j.clients.FmpCalendarClient;
import dev.sorn.fmp4j.clients.FmpChartClient;
import dev.sorn.fmp4j.clients.FmpCompanyClient;
Expand All @@ -23,6 +24,7 @@ public class FmpClient {
protected final FmpHttpClient fmpHttpClient;

// Alphabetical order
protected final FmpBulkClient fmpBulkClient;
protected final FmpCalendarClient fmpCalendarClient;
protected final FmpChartClient fmpChartClient;
protected final FmpCompanyClient fmpCompanyClient;
Expand All @@ -46,6 +48,7 @@ public FmpClient(FmpConfig fmpConfig, FmpHttpClient fmpHttpClient) {
fmpHttpClient,

// Alphabetical order
new FmpBulkClient(fmpConfig, fmpHttpClient),
new FmpCalendarClient(fmpConfig, fmpHttpClient),
new FmpChartClient(fmpConfig, fmpHttpClient),
new FmpCompanyClient(fmpConfig, fmpHttpClient),
Expand All @@ -65,6 +68,7 @@ public FmpClient(
FmpHttpClient fmpHttpClient,

// Alphabetical order
FmpBulkClient fmpBulkClient,
FmpCalendarClient fmpCalendarClient,
FmpChartClient fmpChartClient,
FmpCompanyClient fmpCompanyClient,
Expand All @@ -80,6 +84,7 @@ public FmpClient(
// Alphabetical order
this.fmpConfig = fmpConfig;
this.fmpHttpClient = fmpHttpClient;
this.fmpBulkClient = fmpBulkClient;
this.fmpCalendarClient = fmpCalendarClient;
this.fmpChartClient = fmpChartClient;
this.fmpCompanyClient = fmpCompanyClient;
Expand All @@ -94,6 +99,10 @@ public FmpClient(
this.fmpStatementClient = fmpStatementClient;
}

public FmpBulkClient bulk() {
return fmpBulkClient;
}

public FmpCalendarClient calendar() {
return fmpCalendarClient;
}
Expand Down
25 changes: 25 additions & 0 deletions src/main/java/dev/sorn/fmp4j/clients/FmpBulkClient.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package dev.sorn.fmp4j.clients;

import dev.sorn.fmp4j.cfg.FmpConfig;
import dev.sorn.fmp4j.http.FmpHttpClient;
import dev.sorn.fmp4j.models.FmpCashFlowStatement;
import dev.sorn.fmp4j.services.FmpCashFlowStatementBulkService;
import dev.sorn.fmp4j.services.FmpService;
import dev.sorn.fmp4j.types.FmpPeriod;
import dev.sorn.fmp4j.types.FmpYear;

public class FmpBulkClient {

protected final FmpService<FmpCashFlowStatement[]> cashFlowStatementBulkService;

public FmpBulkClient(FmpConfig fmpConfig, FmpHttpClient fmpHttpClient) {
this.cashFlowStatementBulkService = new FmpCashFlowStatementBulkService(fmpConfig, fmpHttpClient);
}

public synchronized FmpCashFlowStatement[] cashFlowStatements(FmpYear year, FmpPeriod period) {
cashFlowStatementBulkService.param("year", year);
cashFlowStatementBulkService.param("period", period);

return cashFlowStatementBulkService.download();
}
}
58 changes: 58 additions & 0 deletions src/main/java/dev/sorn/fmp4j/csv/FmpCsvDeserializer.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package dev.sorn.fmp4j.csv;

import com.fasterxml.jackson.core.json.JsonReadFeature;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.MappingIterator;
import com.fasterxml.jackson.databind.ObjectReader;
import com.fasterxml.jackson.dataformat.csv.CsvMapper;
import com.fasterxml.jackson.dataformat.csv.CsvSchema;
import dev.sorn.fmp4j.deserialize.FmpDeserializer;
import dev.sorn.fmp4j.json.FmpJsonModule;
import java.io.IOException;
import java.io.StringReader;
import java.lang.reflect.Array;

public final class FmpCsvDeserializer implements FmpDeserializer {
public static final FmpCsvDeserializer FMP_CSV_DESERIALIZER = new FmpCsvDeserializer();

private static final CsvMapper CSV_MAPPER = (CsvMapper) new CsvMapper()
.findAndRegisterModules()
.configure(com.fasterxml.jackson.databind.DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
.configure(com.fasterxml.jackson.databind.DeserializationFeature.ACCEPT_EMPTY_STRING_AS_NULL_OBJECT, true)
.configure(JsonReadFeature.ALLOW_TRAILING_COMMA.mappedFeature(), true)
.registerModule(new FmpJsonModule());

private FmpCsvDeserializer() {}

private String removeByteOrderMark(String content) {
if (content.startsWith("\uFEFF")) {
return content.substring(1);
} else {
return content;
}
}

@Override
public <T> T deserialize(String content, TypeReference<T> type) {
try {
String cleanedCsv = removeByteOrderMark(content);
JavaType javaType = CSV_MAPPER.getTypeFactory().constructType(type.getType());

JavaType componentType = javaType.getContentType();
CsvSchema schema = CsvSchema.emptySchema().withHeader().withNullValue("");
ObjectReader reader = CSV_MAPPER.readerFor(componentType).with(schema);
MappingIterator<?> iterator = reader.readValues(new StringReader(cleanedCsv));
var list = iterator.readAll();

Object array = Array.newInstance(componentType.getRawClass(), list.size());
for (int i = 0; i < list.size(); i++) {
Array.set(array, i, list.get(i));
}
return (T) array;
} catch (IOException e) {
throw new FmpCsvException(
e, "Failed to deserialize CSV to '%s': %s", type.getType().getTypeName(), content);
}
}
}
9 changes: 9 additions & 0 deletions src/main/java/dev/sorn/fmp4j/csv/FmpCsvException.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package dev.sorn.fmp4j.csv;

import static java.lang.String.format;

public class FmpCsvException extends RuntimeException {
public FmpCsvException(Throwable t, String message, Object... args) {
super(format(message, args), t);
}
}
7 changes: 7 additions & 0 deletions src/main/java/dev/sorn/fmp4j/deserialize/FmpDeserializer.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package dev.sorn.fmp4j.deserialize;

import com.fasterxml.jackson.core.type.TypeReference;

public interface FmpDeserializer {
<T> T deserialize(String content, TypeReference<T> type);
}
36 changes: 30 additions & 6 deletions src/main/java/dev/sorn/fmp4j/http/FmpHttpClientImpl.java
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
package dev.sorn.fmp4j.http;

import static dev.sorn.fmp4j.csv.FmpCsvDeserializer.FMP_CSV_DESERIALIZER;
import static dev.sorn.fmp4j.http.FmpUriUtils.uriWithParams;
import static dev.sorn.fmp4j.json.FmpJsonDeserializerImpl.FMP_JSON_DESERIALIZER;
import static dev.sorn.fmp4j.json.FmpJsonDeserializer.FMP_JSON_DESERIALIZER;
import static java.util.Objects.requireNonNull;

import com.fasterxml.jackson.core.type.TypeReference;
import dev.sorn.fmp4j.csv.FmpCsvDeserializer;
import dev.sorn.fmp4j.csv.FmpCsvException;
import dev.sorn.fmp4j.json.FmpJsonDeserializer;
import dev.sorn.fmp4j.json.FmpJsonException;
import dev.sorn.fmp4j.types.FmpApiKey;
Expand All @@ -23,15 +26,22 @@
public class FmpHttpClientImpl implements FmpHttpClient {
public static final FmpHttpClient FMP_HTTP_CLIENT = new FmpHttpClientImpl();
private final HttpClient http;
private final FmpJsonDeserializer deserializer;
private final FmpJsonDeserializer jsonDeserializer;
private final FmpCsvDeserializer csvDeserializer;

private FmpHttpClientImpl() {
this(HttpClients.createDefault(), FMP_JSON_DESERIALIZER);
this(HttpClients.createDefault(), FMP_JSON_DESERIALIZER, FMP_CSV_DESERIALIZER);
}

public FmpHttpClientImpl(HttpClient httpClient, FmpJsonDeserializer deserializer) {
public FmpHttpClientImpl(HttpClient httpClient, FmpJsonDeserializer jsonDeserializer) {
this(httpClient, jsonDeserializer, FMP_CSV_DESERIALIZER);
}

public FmpHttpClientImpl(
HttpClient httpClient, FmpJsonDeserializer jsonDeserializer, FmpCsvDeserializer csvDeserializer) {
this.http = requireNonNull(httpClient, "'httpClient' is required");
this.deserializer = requireNonNull(deserializer, "'deserializer' is required");
this.jsonDeserializer = requireNonNull(jsonDeserializer, "'jsonDeserializer' is required");
this.csvDeserializer = requireNonNull(csvDeserializer, "'csvDeserializer' is required");
}

@Override
Expand All @@ -46,7 +56,13 @@ public <T> T get(TypeReference<T> type, URI uri, Map<String, String> headers, Ma
"Unauthorized for type [%s], uri [%s], headers [%s], queryParams [%s];\nresponseBody: %s",
type.getType(), uri, headers, queryParams, responseBody);
}
return deserializer.fromJson(responseBody, type);

String contentType = headers != null ? headers.get("Content-Type") : null;
if ("text/csv".equals(contentType)) {
return csvDeserializer.deserialize(responseBody, type);
} else {
return jsonDeserializer.deserialize(responseBody, type);
}
} catch (FmpUnauthorizedException e) {
throw e;
} catch (FmpJsonException e) {
Expand All @@ -57,6 +73,14 @@ public <T> T get(TypeReference<T> type, URI uri, Map<String, String> headers, Ma
uri,
headers,
queryParams);
} catch (FmpCsvException e) {
throw new FmpHttpException(
e,
"CSV deserialization failed for type [%s], uri [%s], headers [%s], queryParams [%s]",
type.getType(),
uri,
headers,
queryParams);
} catch (ParseException | IOException | RuntimeException e) {
throw new FmpHttpException(e, "HTTP request failed: %s", uri);
}
Expand Down
28 changes: 26 additions & 2 deletions src/main/java/dev/sorn/fmp4j/json/FmpJsonDeserializer.java
Original file line number Diff line number Diff line change
@@ -1,7 +1,31 @@
package dev.sorn.fmp4j.json;

import com.fasterxml.jackson.core.type.TypeReference;
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 dev.sorn.fmp4j.deserialize.FmpDeserializer;
import java.io.IOException;

public interface FmpJsonDeserializer {
<T> T fromJson(String json, TypeReference<T> type);
public final class FmpJsonDeserializer implements FmpDeserializer {
public static final FmpJsonDeserializer FMP_JSON_DESERIALIZER = new FmpJsonDeserializer();
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper()
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
.disable(SerializationFeature.FAIL_ON_EMPTY_BEANS)
.registerModule(new JavaTimeModule())
.registerModule(new FmpJsonModule());

private FmpJsonDeserializer() {
// prevent direct instantiation
}

public <T> T deserialize(String json, TypeReference<T> type) {
try {
return OBJECT_MAPPER.readValue(json, type);
} catch (IOException e) {
throw new FmpJsonException(
e, "Failed to deserialize JSON to '%s': %s", type.getType().getTypeName(), json);
}
}
}
30 changes: 0 additions & 30 deletions src/main/java/dev/sorn/fmp4j/json/FmpJsonDeserializerImpl.java

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package dev.sorn.fmp4j.services;

import static dev.sorn.fmp4j.json.FmpJsonUtils.typeRef;

import dev.sorn.fmp4j.cfg.FmpConfig;
import dev.sorn.fmp4j.http.FmpHttpClient;
import dev.sorn.fmp4j.models.FmpCashFlowStatement;
import dev.sorn.fmp4j.types.FmpPeriod;
import dev.sorn.fmp4j.types.FmpYear;
import java.util.Map;

public class FmpCashFlowStatementBulkService extends FmpService<FmpCashFlowStatement[]> {
public FmpCashFlowStatementBulkService(FmpConfig cfg, FmpHttpClient http) {
super(cfg, http, typeRef(FmpCashFlowStatement[].class));
}

@Override
protected String relativeUrl() {
return "/cash-flow-statement-bulk";
}

@Override
protected Map<String, Class<?>> requiredParams() {
return Map.of("year", FmpYear.class, "period", FmpPeriod.class);
}

@Override
protected Map<String, Class<?>> optionalParams() {
return Map.of();
}

@Override
protected Map<String, String> headers() {
return Map.of("Content-Type", "text/csv");
}
}
2 changes: 1 addition & 1 deletion src/main/java/dev/sorn/fmp4j/services/FmpService.java
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ private void validateParamType(String key, Object value) {
}
}

protected final Map<String, String> headers() {
protected Map<String, String> headers() {
return Map.of("Content-Type", "application/json");
}

Expand Down
Loading
Loading