Skip to content
Merged
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 config/checkstyle/checkstyle-suppressions.xml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
<suppress files=".*TestData\.java" checks=".*"/>
<suppress files=".*TestUtils\.java" checks=".*"/>
<suppress files=".*Test\.java" checks="AnnotationUseStyle"/>
<suppress files=".*Test\.java" checks="AvoidInlineConditionals"/>
<suppress files=".*Test\.java" checks="MagicNumber"/>
<suppress files=".*Test\.java" checks="MethodName"/>
<suppress files=".*Defs\.java" checks=".*"/>
Expand Down
7 changes: 0 additions & 7 deletions src/main/java/dev/sorn/fmp4j/FmpClient.java
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
package dev.sorn.fmp4j;

import static dev.sorn.fmp4j.http.FmpHttpClientImpl.FMP_HTTP_CLIENT;

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;
Expand Down Expand Up @@ -38,10 +35,6 @@ public class FmpClient {
protected final FmpSecFilingsSearchClient fmpSecFilingsSearchClient;
protected final FmpStatementClient fmpStatementClient;

public FmpClient() {
this(new FmpConfigImpl(), FMP_HTTP_CLIENT);
}

public FmpClient(FmpConfig fmpConfig, FmpHttpClient fmpHttpClient) {
this(
fmpConfig,
Expand Down
4 changes: 2 additions & 2 deletions src/main/java/dev/sorn/fmp4j/clients/FmpQuoteClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@
import dev.sorn.fmp4j.models.FmpFullQuote;
import dev.sorn.fmp4j.models.FmpPartialQuote;
import dev.sorn.fmp4j.models.FmpStockPriceChange;
import dev.sorn.fmp4j.services.FmpPartialQuoteService;
import dev.sorn.fmp4j.services.FmpQuoteService;
import dev.sorn.fmp4j.services.FmpService;
import dev.sorn.fmp4j.services.FmpShortQuoteService;
import dev.sorn.fmp4j.services.FmpStockPriceChangeService;
import dev.sorn.fmp4j.types.FmpSymbol;

Expand All @@ -18,7 +18,7 @@ public class FmpQuoteClient {

public FmpQuoteClient(FmpConfig fmpConfig, FmpHttpClient fmpHttpClient) {
this.quoteService = new FmpQuoteService(fmpConfig, fmpHttpClient);
this.shortQuoteService = new FmpShortQuoteService(fmpConfig, fmpHttpClient);
this.shortQuoteService = new FmpPartialQuoteService(fmpConfig, fmpHttpClient);
this.stockPriceChangeService = new FmpStockPriceChangeService(fmpConfig, fmpHttpClient);
}

Expand Down
8 changes: 5 additions & 3 deletions src/main/java/dev/sorn/fmp4j/csv/FmpCsvDeserializer.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import com.fasterxml.jackson.core.json.JsonReadFeature;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.dataformat.csv.CsvMapper;
import dev.sorn.fmp4j.exceptions.FmpDeserializationException;
import dev.sorn.fmp4j.http.FmpDeserializer;
import dev.sorn.fmp4j.json.FmpJsonModule;
import java.io.IOException;
Expand All @@ -14,15 +15,16 @@

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 FmpCsvDeserializer() {
// prevent direct instantiation
}

private String removeByteOrderMark(String content) {
if (content.startsWith("\uFEFF")) {
Expand All @@ -46,7 +48,7 @@ public <T> T deserialize(String content, TypeReference<T> type) {
range(0, list.size()).forEach(i -> Array.set(array, i, list.get(i)));
return (T) array;
} catch (IOException e) {
throw new FmpCsvException(
throw new FmpDeserializationException(
e, "Failed to deserialize CSV to '%s': %s", type.getType().getTypeName(), content);
}
}
Expand Down
9 changes: 0 additions & 9 deletions src/main/java/dev/sorn/fmp4j/csv/FmpCsvException.java

This file was deleted.

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

public class FmpDeserializationException extends FmpException {
public FmpDeserializationException(Throwable cause, String message, Object... args) {
super(cause, message, args);
}

public FmpDeserializationException(String message, Object... args) {
super(message, args);
}
}
4 changes: 4 additions & 0 deletions src/main/java/dev/sorn/fmp4j/exceptions/FmpException.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@
import static java.lang.String.format;

public class FmpException extends RuntimeException {
public FmpException(Throwable t, String message, Object... args) {
super(format(message, args), t);
}

public FmpException(String message, Object... args) {
super(format(message, args));
}
Expand Down
27 changes: 27 additions & 0 deletions src/main/java/dev/sorn/fmp4j/http/FmpContentType.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package dev.sorn.fmp4j.http;

import dev.sorn.fmp4j.types.FmpValueObject;

public enum FmpContentType implements FmpValueObject<String> {
CSV("text/csv"),
JSON("application/json");

private final String value;

FmpContentType(String value) {
this.value = value;
}

public static FmpContentType fromContentTypeHeader(String header) {
for (final var contentType : values()) {
if (header != null && header.equals(contentType.value())) {
return contentType;
}
}
throw new IllegalArgumentException("Unsupported content type: " + header);
}

public String value() {
return value;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package dev.sorn.fmp4j.http;

@FunctionalInterface
public interface FmpDeserializationRegistry {
<T> FmpDeserializer resolve(FmpContentType contentType);
}
65 changes: 21 additions & 44 deletions src/main/java/dev/sorn/fmp4j/http/FmpHttpClientImpl.java
Original file line number Diff line number Diff line change
@@ -1,15 +1,10 @@
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.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.exceptions.FmpDeserializationException;
import dev.sorn.fmp4j.types.FmpApiKey;
import java.io.IOException;
import java.net.URI;
Expand All @@ -18,65 +13,47 @@
import org.apache.commons.lang3.tuple.Pair;
import org.apache.hc.client5.http.classic.HttpClient;
import org.apache.hc.client5.http.classic.methods.HttpGet;
import org.apache.hc.client5.http.impl.classic.HttpClients;
import org.apache.hc.core5.http.ClassicHttpResponse;
import org.apache.hc.core5.http.ParseException;
import org.apache.hc.core5.http.io.entity.EntityUtils;

public class FmpHttpClientImpl implements FmpHttpClient {
public static final FmpHttpClient FMP_HTTP_CLIENT = new FmpHttpClientImpl();
public final class FmpHttpClientImpl implements FmpHttpClient {
private final HttpClient http;
private final FmpJsonDeserializer jsonDeserializer;
private final FmpCsvDeserializer csvDeserializer;
private final FmpDeserializationRegistry deserializationRegistry;

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

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

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

@Override
public <T> T get(TypeReference<T> type, URI uri, Map<String, String> headers, Map<String, Object> queryParams) {
try {
requireNonNull(headers, "'headers' is required");
final var contentType = headers.get("Content-Type");
final var request = buildRequest(uri, headers, queryParams);
final var responsePair = executeRequest(request);
final var statusCode = responsePair.getLeft();
final var responseBody = responsePair.getRight();
if (responseBody.isBlank()) {
throw new FmpHttpException(
"Empty response for type [%s], uri [%s], headers [%s], queryParams [%s]",
type.getType(), uri, headers, queryParams);
}
if (statusCode == 401 || statusCode == 403) {
throw new FmpUnauthorizedException(
"Unauthorized for type [%s], uri [%s], headers [%s], queryParams [%s];\nresponseBody: %s",
"Unauthorized for type [%s], uri [%s], headers [%s], queryParams [%s]",
type.getType(), uri, headers, queryParams, responseBody);
}

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) {
return deserializationRegistry
.resolve(FmpContentType.fromContentTypeHeader(contentType))
.deserialize(responseBody, type);
} catch (FmpHttpException e) {
throw e;
} catch (FmpJsonException e) {
throw new FmpHttpException(
e,
"JSON deserialization failed for type [%s], uri [%s], headers [%s], queryParams [%s]",
type.getType(),
uri,
headers,
queryParams);
} catch (FmpCsvException e) {
} catch (FmpDeserializationException e) {
throw new FmpHttpException(
e,
"CSV deserialization failed for type [%s], uri [%s], headers [%s], queryParams [%s]",
"Deserialization failed for type [%s], uri [%s], headers [%s], queryParams [%s]",
type.getType(),
uri,
headers,
Expand All @@ -86,7 +63,7 @@ public <T> T get(TypeReference<T> type, URI uri, Map<String, String> headers, Ma
}
}

protected HttpGet buildRequest(URI uri, Map<String, String> headers, Map<String, Object> queryParams) {
HttpGet buildRequest(URI uri, Map<String, String> headers, Map<String, Object> queryParams) {
final var copy = new HashMap<>(queryParams);
final var key = (FmpApiKey) queryParams.get("apikey");
copy.put("apikey", key.value());
Expand All @@ -98,7 +75,7 @@ protected HttpGet buildRequest(URI uri, Map<String, String> headers, Map<String,
return request;
}

protected Pair<Integer, String> executeRequest(HttpGet request) throws IOException, ParseException {
Pair<Integer, String> executeRequest(HttpGet request) throws IOException, ParseException {
try (ClassicHttpResponse response = http.executeOpen(null, request, null)) {
return Pair.of(response.getCode(), EntityUtils.toString(response.getEntity()));
}
Expand Down
3 changes: 2 additions & 1 deletion src/main/java/dev/sorn/fmp4j/json/FmpJsonDeserializer.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import dev.sorn.fmp4j.exceptions.FmpDeserializationException;
import dev.sorn.fmp4j.http.FmpDeserializer;
import java.io.IOException;

Expand All @@ -24,7 +25,7 @@ public <T> T deserialize(String json, TypeReference<T> type) {
try {
return OBJECT_MAPPER.readValue(json, type);
} catch (IOException e) {
throw new FmpJsonException(
throw new FmpDeserializationException(
e, "Failed to deserialize JSON to '%s': %s", type.getType().getTypeName(), json);
}
}
Expand Down
9 changes: 0 additions & 9 deletions src/main/java/dev/sorn/fmp4j/json/FmpJsonException.java

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@
import dev.sorn.fmp4j.types.FmpSymbol;
import java.util.Map;

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

Expand Down
10 changes: 6 additions & 4 deletions src/main/java/dev/sorn/fmp4j/services/FmpService.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package dev.sorn.fmp4j.services;

import static java.util.Objects.requireNonNull;

import com.fasterxml.jackson.core.type.TypeReference;
import dev.sorn.fmp4j.cfg.FmpConfig;
import dev.sorn.fmp4j.http.FmpHttpClient;
Expand All @@ -16,10 +18,10 @@ public abstract class FmpService<R> {
protected final TypeReference<R> typeRef;

protected FmpService(FmpConfig cfg, FmpHttpClient http, TypeReference<R> typeRef) {
this.cfg = cfg;
this.http = http;
this.typeRef = typeRef;
this.params.put("apikey", cfg.fmpApiKey());
this.cfg = requireNonNull(cfg, "'cfg' is required");
this.http = requireNonNull(http, "'http' is required");
this.typeRef = requireNonNull(typeRef, "'typeRef' is required");
this.params.put("apikey", requireNonNull(cfg.fmpApiKey(), "'apikey' is required"));
}

protected abstract String relativeUrl();
Expand Down
Loading
Loading