argos/dmp-backend/core/src/main/java/eu/eudat/service/externalfetcher/ExternalFetcherServiceImpl....

368 lines
21 KiB
Java

package eu.eudat.service.externalfetcher;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.jayway.jsonpath.DocumentContext;
import com.jayway.jsonpath.JsonPath;
import com.jayway.jsonpath.PathNotFoundException;
import eu.eudat.commons.JsonHandlingService;
import eu.eudat.commons.enums.ExternalFetcherSourceType;
import eu.eudat.commons.types.externalfetcher.StaticOptionEntity;
import eu.eudat.convention.ConventionService;
import eu.eudat.data.ReferenceEntity;
import eu.eudat.model.Reference;
import eu.eudat.model.referencedefinition.Field;
import eu.eudat.service.externalfetcher.config.entities.*;
import eu.eudat.service.externalfetcher.models.ExternalDataResult;
import eu.eudat.service.externalfetcher.criteria.ExternalReferenceCriteria;
import gr.cite.tools.exception.MyApplicationException;
import io.netty.handler.ssl.SslContext;
import io.netty.handler.ssl.SslContextBuilder;
import io.netty.handler.ssl.util.InsecureTrustManagerFactory;
import net.minidev.json.JSONArray;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.*;
import org.springframework.http.client.reactive.ReactorClientHttpConnector;
import org.springframework.http.codec.json.Jackson2JsonDecoder;
import org.springframework.stereotype.Service;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.netty.http.client.HttpClient;
import javax.net.ssl.SSLException;
import java.util.*;
import java.util.stream.Collectors;
@Service
public class ExternalFetcherServiceImpl implements ExternalFetcherService {
private static final Logger logger = LoggerFactory.getLogger(ExternalFetcherServiceImpl.class);
private WebClient webClient;
private final ConventionService conventionService;
private final JsonHandlingService jsonHandlingService;
@Autowired
public ExternalFetcherServiceImpl(ConventionService conventionService, JsonHandlingService jsonHandlingService) {
this.conventionService = conventionService;
this.jsonHandlingService = jsonHandlingService;
}
private WebClient getWebClient() {
if (this.webClient == null) {
this.webClient = WebClient.builder().codecs(clientCodecConfigurer -> {
clientCodecConfigurer.defaultCodecs().jackson2JsonDecoder(new Jackson2JsonDecoder(new ObjectMapper(), MediaType.APPLICATION_JSON));
clientCodecConfigurer.defaultCodecs().maxInMemorySize(2 * ((int) Math.pow(1024, 3))); //GK: Why here???
}
).clientConnector(new ReactorClientHttpConnector(HttpClient.create().followRedirect(true))).build();
}
return webClient;
}
@Override
public ExternalDataResult getExternalData(List<SourceBaseConfiguration> sources, ExternalReferenceCriteria externalReferenceCriteria, String key) {
List<SourceBaseConfiguration> apiSourcesToUse = sources;
if (!this.conventionService.isNullOrEmpty(key)){
apiSourcesToUse = sources.stream().filter(x-> x.getKey().equals(key)).collect(Collectors.toList());
}
if (this.conventionService.isListNullOrEmpty(apiSourcesToUse)) return new ExternalDataResult();
apiSourcesToUse.sort(Comparator.comparing(SourceBaseConfiguration::getOrdinal));
return this.queryExternalData(sources, externalReferenceCriteria);
}
@Override
public Integer countExternalData(List<SourceBaseConfiguration> sources, ExternalReferenceCriteria externalReferenceCriteria, String key) {
return getExternalData(sources, externalReferenceCriteria, key).getResults().size();
}
private ExternalDataResult queryExternalData(List<SourceBaseConfiguration> sources, ExternalReferenceCriteria externalReferenceCriteria) {
ExternalDataResult results = new ExternalDataResult();
if (this.conventionService.isListNullOrEmpty(sources)) return new ExternalDataResult();
for (SourceBaseConfiguration source : sources) {
if (source.getType() == null || source.getType().equals(ExternalFetcherSourceType.API)) {
try {
SourceExternalApiConfiguration<ResultsConfiguration<ResultFieldsMappingConfiguration>, AuthenticationConfiguration, QueryConfig<QueryCaseConfig>> apiSource = (SourceExternalApiConfiguration)source;
// this.applyFunderToQuery(apiSource, externalReferenceCriteria);
String auth = null;
if (apiSource.getAuth() != null && apiSource.getAuth().getEnabled() != null && apiSource.getAuth().getEnabled()) {
auth = this.buildAuthentication(apiSource.getAuth());
}
results.addAll(this.queryExternalData(apiSource, externalReferenceCriteria, auth));
} catch (Exception e) {
logger.error(e.getLocalizedMessage(), e);
}
} else if (source.getType() != null && source.getType().equals(ExternalFetcherSourceType.STATIC)) {
SourceStaticOptionConfiguration<Static> staticSource = (SourceStaticOptionConfiguration)source;
results.addAll(queryStaticData(staticSource, externalReferenceCriteria));
}
}
return results;
}
private ExternalDataResult queryStaticData(SourceStaticOptionConfiguration<Static> staticSource, ExternalReferenceCriteria externalReferenceCriteria){
ExternalDataResult externalDataResult = new ExternalDataResult();
externalDataResult.setRawData(new ArrayList<>());
externalDataResult.setResults(new ArrayList<>());
for (Static item : staticSource.getItems()){
if (this.conventionService.isListNullOrEmpty(item.getOptions())) continue;
Map<String, String> result = new HashMap<>();
Map<String, Object> rawData = new HashMap<>();
for (Object object: item.getOptions()) {
StaticOptionEntity staticOption = (StaticOptionEntity) object;
if (this.conventionService.isNullOrEmpty(externalReferenceCriteria.getLike())){
rawData.put(staticOption.getCode(), staticOption.getValue());
result.put(staticOption.getCode(), staticOption.getValue());
result.put(ReferenceEntity.KnownFields.SourceLabel, staticSource.getLabel());
result.put(ReferenceEntity.KnownFields.Key, staticSource.getKey());
}else if (!this.conventionService.isNullOrEmpty(externalReferenceCriteria.getLike()) && externalReferenceCriteria.getLike().toUpperCase().contains(staticOption.getValue().toUpperCase())){
rawData.put(staticOption.getCode(), staticOption.getValue());
result.put(staticOption.getCode(), staticOption.getValue());
result.put(ReferenceEntity.KnownFields.SourceLabel, staticSource.getLabel());
result.put(ReferenceEntity.KnownFields.Key, staticSource.getKey());
}
}
if (!rawData.isEmpty()) externalDataResult.getRawData().add(rawData);
if (!result.isEmpty()) externalDataResult.getResults().add(result);
}
return externalDataResult;
}
private String buildAuthentication(AuthenticationConfiguration authenticationConfiguration) {
HttpMethod method;
switch (authenticationConfiguration.getAuthMethod()) {
case GET -> method = HttpMethod.GET;
case POST -> method =HttpMethod.POST;
default -> throw new MyApplicationException("unrecognized type " + authenticationConfiguration.getAuthMethod());
}
Map<String, Object> response = this.getWebClient().method(method).uri(authenticationConfiguration.getAuthUrl())
.contentType(MediaType.APPLICATION_JSON)
.bodyValue(this.parseBodyString(authenticationConfiguration.getAuthRequestBody()))
.exchangeToMono(mono -> mono.bodyToMono(new ParameterizedTypeReference<Map<String, Object>>() {
})).block();
if (response == null) throw new MyApplicationException("Authentication " + authenticationConfiguration.getAuthUrl() + " failed");
return authenticationConfiguration.getType() + " " + response.getOrDefault(authenticationConfiguration.getAuthTokenPath(), null);
}
private String replaceLookupFieldQuery(String query, ExternalReferenceCriteria externalReferenceCriteria, List<QueryConfig<QueryCaseConfig>> queryConfigs) {
String finalQuery = query;
String likeValue = this.conventionService.isNullOrEmpty(externalReferenceCriteria.getLike()) ? "" : externalReferenceCriteria.getLike();
List<Reference> referenceList = this.conventionService.isListNullOrEmpty(externalReferenceCriteria.getDependencyReferences()) ? new ArrayList<>() : externalReferenceCriteria.getDependencyReferences().stream()
.filter(x-> x.getDefinition() != null && x.getType() != null && !this.conventionService.isListNullOrEmpty(x.getDefinition().getFields())).toList();
if (this.conventionService.isListNullOrEmpty(queryConfigs)) return query;
for (QueryConfig<?> queryConfig : queryConfigs){
Comparator<QueryCaseConfig> queryCaseConfigcomparator = Comparator.comparing(x-> x.getReferenceTypeId() == null ? 0 : 1); //Reference QueryCaseConfig are more important
QueryCaseConfig caseConfig = this.conventionService.isListNullOrEmpty(queryConfig.getCases()) ? null : queryConfig.getCases().stream().filter(x ->
(this.conventionService.isNullOrEmpty(x.getLikePattern()) || likeValue.matches(x.getLikePattern()))
&& ((x.getReferenceTypeId() == null && this.conventionService.isNullOrEmpty(x.getReferenceTypeSourceKey())) || referenceList.stream().anyMatch(y -> Objects.equals(y.getType().getId(), x.getReferenceTypeId()) && Objects.equals(y.getSource(), x.getReferenceTypeSourceKey())))
).max(queryCaseConfigcomparator).orElse(null);
String filterValue = queryConfig.getDefaultValue();
if (caseConfig != null){
filterValue = caseConfig.getValue();
if (caseConfig.getReferenceTypeId() != null && !this.conventionService.isNullOrEmpty(caseConfig.getReferenceTypeSourceKey()) ){
Reference dependencyReference = referenceList.stream()
.filter(x-> Objects.equals(x.getType().getId(), caseConfig.getReferenceTypeId()) && Objects.equals(x.getSource() ,caseConfig.getReferenceTypeSourceKey())).findFirst().orElse(null);
if (dependencyReference != null){
for (Field field : dependencyReference.getDefinition().getFields()){
filterValue = filterValue.replaceAll("{" + field.getCode() + "}", field.getValue());
}
}
} else if (!this.conventionService.isNullOrEmpty(likeValue)) {
if (caseConfig.getSeparator() != null) {
String[] likes = likeValue.split(caseConfig.getSeparator());
for (int i = 0; i < likes.length; i++) {
filterValue = filterValue.replaceAll("\\{like" + (i + 1) + "}", likes[i]);
}
} else {
filterValue = filterValue.replaceAll("\\{like}", likeValue);
}
} else {
filterValue = queryConfig.getDefaultValue() == null ? "" : queryConfig.getDefaultValue();
}
}
finalQuery = finalQuery.replaceAll("\\{" + queryConfig.getName() + "}", filterValue);
}
return finalQuery;
}
protected String replaceLookupFields(String path, final SourceExternalApiConfiguration<ResultsConfiguration<ResultFieldsMappingConfiguration>, AuthenticationConfiguration, QueryConfig<QueryCaseConfig>> apiSource, ExternalReferenceCriteria externalReferenceCriteria) {
if (this.conventionService.isNullOrEmpty(path)) return path;
String completedPath = path;
if (!this.conventionService.isListNullOrEmpty(apiSource.getQueries())){
completedPath = this.replaceLookupFieldQuery(completedPath, externalReferenceCriteria, apiSource.getQueries());
}
if (!this.conventionService.isNullOrEmpty(externalReferenceCriteria.getPage())) completedPath = completedPath.replace("{page}", externalReferenceCriteria.getPage());
else if (!this.conventionService.isNullOrEmpty(apiSource.getFirstPage())) completedPath = completedPath.replace("{page}", apiSource.getFirstPage());
else completedPath = completedPath.replace("{page}", "1");
completedPath = completedPath.replace("{pageSize}", !this.conventionService.isNullOrEmpty(externalReferenceCriteria.getPageSize()) ? externalReferenceCriteria.getPageSize() : "60");
completedPath = completedPath.replace("{host}", !this.conventionService.isNullOrEmpty(externalReferenceCriteria.getHost()) ? externalReferenceCriteria.getHost() : "");
completedPath = completedPath.replace("{path}", !this.conventionService.isNullOrEmpty(externalReferenceCriteria.getPath()) ? externalReferenceCriteria.getPath() : "");
return completedPath;
}
private ExternalDataResult queryExternalData(final SourceExternalApiConfiguration<ResultsConfiguration<ResultFieldsMappingConfiguration>, AuthenticationConfiguration, QueryConfig<QueryCaseConfig>> apiSource, ExternalReferenceCriteria externalReferenceCriteria, String auth) throws Exception {
String replacedPath = replaceLookupFields(apiSource.getUrl(), apiSource, externalReferenceCriteria);
String replacedBody = replaceLookupFields(apiSource.getRequestBody(), apiSource, externalReferenceCriteria);
ExternalDataResult externalDataResult = this.getExternalDataResults(replacedPath, apiSource, replacedBody, auth);
if(externalDataResult != null) {
if (apiSource.getFilterType() != null && apiSource.getFilterType().equals("local") && (externalReferenceCriteria.getLike() != null && !externalReferenceCriteria.getLike().isEmpty())) {
externalDataResult.setResults(externalDataResult.getResults().stream()
.filter(r -> r.get(ReferenceEntity.KnownFields.Label).toLowerCase().contains(externalReferenceCriteria.getLike().toLowerCase()))
.collect(Collectors.toList()));
}
externalDataResult.setResults(externalDataResult.getResults().stream().peek(x -> x.put(ReferenceEntity.KnownFields.SourceLabel, apiSource.getLabel())).peek(x -> x.put(ReferenceEntity.KnownFields.Key, apiSource.getKey())).toList());
return externalDataResult;
}
else {
return new ExternalDataResult();
}
}
protected ExternalDataResult getExternalDataResults(String urlString, final SourceExternalApiConfiguration<ResultsConfiguration<ResultFieldsMappingConfiguration>, AuthenticationConfiguration, QueryConfig<QueryCaseConfig>> apiSource, String requestBody, String auth) {
try {
JsonNode jsonBody = new ObjectMapper().readTree(requestBody);
HttpMethod method;
switch (apiSource.getHttpMethod()) {
case GET -> method = HttpMethod.GET;
case POST -> method =HttpMethod.POST;
default -> throw new MyApplicationException("unrecognized type " + apiSource.getHttpMethod());
}
ResponseEntity<String> response = this.getWebClient().method(method).uri(urlString).headers(httpHeaders -> {
if (this.conventionService.isNullOrEmpty(apiSource.getContentType())) {
httpHeaders.setAccept(Collections.singletonList(MediaType.valueOf(apiSource.getContentType())));
httpHeaders.setContentType(MediaType.valueOf(apiSource.getContentType()));
}
if (auth != null) {
httpHeaders.set("Authorization", auth);
}
}).bodyValue(jsonBody).retrieve().toEntity(String.class).block();
if (response == null || !response.getStatusCode().isSameCodeAs(HttpStatus.OK) || !response.hasBody() || response.getBody() == null) return null;
//do here all the parsing
List<String> responseContentTypeHeader = response.getHeaders().getOrDefault("Content-Type", null);
String responseContentType = !this.conventionService.isListNullOrEmpty(responseContentTypeHeader) && responseContentTypeHeader.getFirst() != null ? responseContentTypeHeader.getFirst() : "";
if (responseContentType.contains("json") ) {
DocumentContext jsonContext = JsonPath.parse(response.getBody());
return this.jsonToExternalDataResult(jsonContext, apiSource.getResults());
} else {
throw new MyApplicationException("Unsupported response type" + responseContentType);
}
} catch (Exception exception) {
logger.error(exception.getMessage(), exception);
}
return null;
}
private ExternalDataResult jsonToExternalDataResult(DocumentContext jsonContext, ResultsConfiguration<ResultFieldsMappingConfiguration> resultsConfigurationEntity) {
ExternalDataResult result = new ExternalDataResult();
if (this.conventionService.isNullOrEmpty(resultsConfigurationEntity.getResultsArrayPath())) return new ExternalDataResult();
Object jsonData = jsonContext.read(resultsConfigurationEntity.getResultsArrayPath());
List<Map<String, Object>> rawData = new ArrayList<>();
if (jsonData instanceof List) {
rawData = (List<Map<String, Object>>) jsonData;
}else{
rawData.add((Map<String, Object>)jsonData);
}
result.setRawData(rawData);
if (this.conventionService.isListNullOrEmpty(rawData) || this.conventionService.isListNullOrEmpty(resultsConfigurationEntity.getFieldsMapping())) return new ExternalDataResult();
List<Map<String, String>> parsedData = new ArrayList<>();
for(Object resultItem : result.getRawData()){
Map<String, String> map = new HashMap<>();
boolean isValid = true;
for(ResultFieldsMappingConfiguration field : resultsConfigurationEntity.getFieldsMapping()) {
if (this.conventionService.isNullOrEmpty(field.getResponsePath()) || this.conventionService.isNullOrEmpty(field.getCode())) continue;
try {
Object value = JsonPath.parse(resultItem).read(field.getResponsePath());
map.put(field.getCode(), normalizeJsonValue(value));
}catch (PathNotFoundException e){
logger.debug("Json Path Error: " + e.getMessage() + " on source " + jsonHandlingService.toJsonSafe(resultItem));
if (ReferenceEntity.KnownFields.ReferenceId.equals(field.getCode())) {
isValid = false;
break;
}
}
}
if (this.conventionService.isNullOrEmpty(map.getOrDefault(ReferenceEntity.KnownFields.ReferenceId, null))){
logger.warn("Invalid reference on source " + jsonHandlingService.toJsonSafe(resultItem));
}
if (isValid) parsedData.add(map);
}
result.setResults(parsedData);
return result;
}
private static String normalizeJsonValue(Object value) {
if (value instanceof JSONArray jsonArray) {
if (!jsonArray.isEmpty() && jsonArray.getFirst() instanceof String) {
return jsonArray.getFirst().toString();
} else {
for (Object o : jsonArray) {
if ((o instanceof Map) && ((Map<?, ?>) o).containsKey("content")) {
try {
return String.valueOf(((Map<?, ?>) o).get("content"));
}
catch (ClassCastException e){
if(((Map<?, ?>) o).get("content") instanceof Integer) {
return String.valueOf(((Map<?, ?>) o).get("content"));
}
return null;
}
}
}
}
} else if (value instanceof Map) {
String key = ((Map<?, ?>)value).containsKey("$") ? "$" : "content";
return String.valueOf(((Map<?, ?>)value).get(key));
}
return value != null ? value.toString() : null;
}
private String parseBodyString(String bodyString) {
String finalBodyString = bodyString;
if (bodyString.contains("{env:")) {
int index = bodyString.indexOf("{env: ");
while (index >= 0) {
int endIndex = bodyString.indexOf("}", index + 6);
String envName = bodyString.substring(index + 6, endIndex);
finalBodyString = finalBodyString.replace("{env: " + envName + "}", System.getenv(envName));
index = bodyString.indexOf("{env: ", index + 6);
}
}
return finalBodyString;
}
}