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 sources, ExternalReferenceCriteria externalReferenceCriteria, String key) { List 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 sources, ExternalReferenceCriteria externalReferenceCriteria, String key) { return getExternalData(sources, externalReferenceCriteria, key).getResults().size(); } private ExternalDataResult queryExternalData(List 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, AuthenticationConfiguration, QueryConfig> 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 staticSource = (SourceStaticOptionConfiguration)source; results.addAll(queryStaticData(staticSource, externalReferenceCriteria)); } } return results; } private ExternalDataResult queryStaticData(SourceStaticOptionConfiguration 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 result = new HashMap<>(); Map 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 response = this.getWebClient().method(method).uri(authenticationConfiguration.getAuthUrl()) .contentType(MediaType.APPLICATION_JSON) .bodyValue(this.parseBodyString(authenticationConfiguration.getAuthRequestBody())) .exchangeToMono(mono -> mono.bodyToMono(new ParameterizedTypeReference>() { })).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> queryConfigs) { String finalQuery = query; String likeValue = this.conventionService.isNullOrEmpty(externalReferenceCriteria.getLike()) ? "" : externalReferenceCriteria.getLike(); List 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 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, AuthenticationConfiguration, QueryConfig> 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, AuthenticationConfiguration, QueryConfig> 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, AuthenticationConfiguration, QueryConfig> 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 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 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 resultsConfigurationEntity) { ExternalDataResult result = new ExternalDataResult(); if (this.conventionService.isNullOrEmpty(resultsConfigurationEntity.getResultsArrayPath())) return new ExternalDataResult(); Object jsonData = jsonContext.read(resultsConfigurationEntity.getResultsArrayPath()); List> rawData = new ArrayList<>(); if (jsonData instanceof List) { rawData = (List>) jsonData; }else{ rawData.add((Map)jsonData); } result.setRawData(rawData); if (this.conventionService.isListNullOrEmpty(rawData) || this.conventionService.isListNullOrEmpty(resultsConfigurationEntity.getFieldsMapping())) return new ExternalDataResult(); List> parsedData = new ArrayList<>(); for(Object resultItem : result.getRawData()){ Map 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; } }