package eu.eudat.service.reference; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.jayway.jsonpath.DocumentContext; import com.jayway.jsonpath.JsonPath; import eu.eudat.authorization.AuthorizationFlags; import eu.eudat.authorization.Permission; import eu.eudat.commons.XmlHandlingService; import eu.eudat.commons.enums.IsActive; import eu.eudat.commons.enums.ReferenceSourceType; import eu.eudat.commons.enums.ReferenceTypeExternalApiHTTPMethodType; import eu.eudat.commons.enums.ReferenceTypeSourceType; import eu.eudat.commons.exceptions.HugeResultSetException; import eu.eudat.commons.types.reference.DefinitionEntity; import eu.eudat.commons.types.reference.FieldEntity; import eu.eudat.commons.types.referencetype.*; import eu.eudat.convention.ConventionService; import eu.eudat.data.ReferenceEntity; import eu.eudat.data.ReferenceTypeEntity; import eu.eudat.model.Reference; import eu.eudat.model.builder.ReferenceBuilder; import eu.eudat.model.builder.referencesearch.ReferenceSearchBuilder; import eu.eudat.model.deleter.ReferenceDeleter; import eu.eudat.model.persist.ReferencePersist; import eu.eudat.model.persist.referencedefinition.DefinitionPersist; import eu.eudat.model.persist.referencedefinition.FieldPersist; import eu.eudat.query.ReferenceQuery; import eu.eudat.query.ReferenceTypeQuery; import eu.eudat.query.lookup.ReferenceDefinitionSearchLookup; import eu.eudat.query.lookup.ReferenceSearchLookup; import eu.eudat.service.remotefetcher.RemoteFetcherService; import eu.eudat.service.remotefetcher.criteria.ExternalReferenceCriteria; import eu.eudat.service.remotefetcher.criteria.FetchStrategy; import eu.eudat.service.remotefetcher.models.ExternalRefernceResult; import gr.cite.commons.web.authz.service.AuthorizationService; import gr.cite.tools.data.builder.BuilderFactory; import gr.cite.tools.data.deleter.DeleterFactory; import gr.cite.tools.data.query.QueryFactory; import gr.cite.tools.exception.MyApplicationException; import gr.cite.tools.exception.MyForbiddenException; import gr.cite.tools.exception.MyNotFoundException; import gr.cite.tools.exception.MyValidationException; import gr.cite.tools.fieldset.BaseFieldSet; import gr.cite.tools.fieldset.FieldSet; import gr.cite.tools.logging.LoggerService; import gr.cite.tools.logging.MapLogEntry; import jakarta.persistence.EntityManager; import jakarta.xml.bind.JAXBException; import net.minidev.json.JSONArray; import org.jetbrains.annotations.NotNull; import org.slf4j.LoggerFactory; import org.springframework.context.MessageSource; import org.springframework.context.i18n.LocaleContextHolder; import org.springframework.core.ParameterizedTypeReference; import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; 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.management.InvalidApplicationException; import javax.xml.parsers.ParserConfigurationException; import javax.xml.transform.TransformerException; import java.time.Instant; import java.util.*; import java.util.stream.Collectors; @Service public class ReferenceServiceImpl implements ReferenceService { private static final LoggerService logger = new LoggerService(LoggerFactory.getLogger(ReferenceServiceImpl.class)); private final RemoteFetcherService remoteFetcherService; private final EntityManager entityManager; private final AuthorizationService authorizationService; private final DeleterFactory deleterFactory; private final BuilderFactory builderFactory; private final ConventionService conventionService; private final MessageSource messageSource; private final QueryFactory queryFactory; private final XmlHandlingService xmlHandlingService; private final WebClient client; public ReferenceServiceImpl(RemoteFetcherService remoteFetcherService, EntityManager entityManager, AuthorizationService authorizationService, DeleterFactory deleterFactory, BuilderFactory builderFactory, ConventionService conventionService, MessageSource messageSource, QueryFactory queryFactory, XmlHandlingService xmlHandlingService) { this.remoteFetcherService = remoteFetcherService; this.entityManager = entityManager; this.authorizationService = authorizationService; this.deleterFactory = deleterFactory; this.builderFactory = builderFactory; this.conventionService = conventionService; this.messageSource = messageSource; this.queryFactory = queryFactory; this.xmlHandlingService = xmlHandlingService; this.client = 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(); } @Override public Reference persist(ReferencePersist model, FieldSet fields) throws MyForbiddenException, MyValidationException, MyApplicationException, MyNotFoundException, InvalidApplicationException, JAXBException, JsonProcessingException, TransformerException, ParserConfigurationException { logger.debug(new MapLogEntry("persisting data").And("model", model).And("fields", fields)); this.authorizationService.authorizeForce(Permission.EditDmpBlueprint); Boolean isUpdate = this.conventionService.isValidGuid(model.getId()); ReferenceEntity data; if (isUpdate) { data = this.entityManager.find(ReferenceEntity.class, model.getId()); if (data == null) throw new MyNotFoundException(messageSource.getMessage("General_ItemNotFound", new Object[]{model.getId(), Reference.class.getSimpleName()}, LocaleContextHolder.getLocale())); } else { data = new ReferenceEntity(); data.setId(UUID.randomUUID()); data.setIsActive(IsActive.Active); data.setCreatedAt(Instant.now()); } data.setLabel(model.getLabel()); data.setType(model.getType()); data.setDescription(model.getDescription()); data.setDefinition(this.xmlHandlingService.toXmlSafe(this.buildDefinitionEntity(model.getDefinition()))); data.setUpdatedAt(Instant.now()); data.setReference(model.getReference()); data.setAbbreviation(model.getAbbreviation()); data.setSource(model.getSource()); data.setSourceType(model.getSourceType()); if (isUpdate) this.entityManager.merge(data); else this.entityManager.persist(data); this.entityManager.flush(); return this.builderFactory.builder(ReferenceBuilder.class).authorize(AuthorizationFlags.OwnerOrDmpAssociatedOrPermissionOrPublic).build(BaseFieldSet.build(fields, Reference._id), data); } private @NotNull DefinitionEntity buildDefinitionEntity(DefinitionPersist persist){ DefinitionEntity data = new DefinitionEntity(); if (persist == null) return data; if (!this.conventionService.isListNullOrEmpty(persist.getFields())){ data.setFields(new ArrayList<>()); for (FieldPersist fieldPersist: persist.getFields()) { data.getFields().add(this.buildFieldEntity(fieldPersist)); } } return data; } private @NotNull FieldEntity buildFieldEntity(FieldPersist persist){ FieldEntity data = new FieldEntity(); if (persist == null) return data; data.setCode(persist.getCode()); data.setDataType(persist.getDataType()); data.setValue(persist.getValue()); return data; } @Override public void deleteAndSave(UUID id) throws MyForbiddenException, InvalidApplicationException { logger.debug("deleting : {}", id); this.authorizationService.authorizeForce(Permission.DeleteReference); this.deleterFactory.deleter(ReferenceDeleter.class).deleteAndSaveByIds(List.of(id)); } // public List searchReference(ReferenceType externalType, String query, String type) throws HugeResultSet, MyNotFoundException, InvalidApplicationException { // ExternalUrlCriteria externalUrlCriteria = new ExternalUrlCriteria(query); // // List> remoteRepos = this.apiContext.getOperationsContext().getRemoteFetcher().get(externalType, externalUrlCriteria, type); // // List list = this.fetchFromDb(externalType, query, type, remoteRepos); // // list.addAll(remoteRepos.stream().map(FetcherReference::fromRemoteModel).toList()); // list = list.stream().filter(x -> x.getName().toLowerCase().contains(query.toLowerCase())).collect(Collectors.toList()); // list.sort(Comparator.comparing(FetcherReference::getName)); // return list; // } @Override public List searchReference(ReferenceSearchLookup lookup) throws HugeResultSetException, MyNotFoundException, InvalidApplicationException { int initialOffset = 0; if (lookup.getPage() != null && !lookup.getPage().isEmpty()){ initialOffset = lookup.getPage().getOffset(); lookup.getPage().setOffset(0); } ExternalReferenceCriteria externalReferenceCriteria = new ExternalReferenceCriteria(lookup.getLike()); List> remoteRepos = remoteFetcherService.getReferences(lookup.getType(), externalReferenceCriteria, lookup.getKey()); List externalModels = this.builderFactory.builder(ReferenceSearchBuilder.class).authorize(AuthorizationFlags.OwnerOrDmpAssociatedOrPermissionOrPublic).build(lookup.getProject(), remoteRepos); List models = this.fetchFromDb(lookup); models.addAll(externalModels); if (!this.conventionService.isNullOrEmpty(lookup.getLike())) { models = models.stream().filter(x -> x.getLabel().toLowerCase().contains(lookup.getLike().toLowerCase())).collect(Collectors.toList()); } models.sort(Comparator.comparing(Reference::getLabel)); if (lookup.getPage() != null && !lookup.getPage().isEmpty()){ models = models.stream().skip(initialOffset).limit(lookup.getPage().getSize()).toList(); } return models; } private List fetchFromDb(ReferenceSearchLookup lookup){ ReferenceQuery query = lookup.enrich(this.queryFactory).authorize(AuthorizationFlags.OwnerOrDmpAssociatedOrPermissionOrPublic).sourceTypes(ReferenceSourceType.Internal); List data = query.collectAs(lookup.getProject()); return this.builderFactory.builder(ReferenceBuilder.class).authorize(AuthorizationFlags.OwnerOrDmpAssociatedOrPermissionOrPublic).build(lookup.getProject(), data); } // private List fetchFromDb(ReferenceType externalType, String query, String type, List> remoteRepos) throws InvalidApplicationException { // List list = new LinkedList<>(); // switch (externalType) { // case DataRepositories: // case PubRepositories: // case Journals: { // DataRepositoryCriteria criteria = new DataRepositoryCriteria(); // if (!query.isEmpty()) criteria.setLike(query); // criteria.setCreationUserId(this.userScope.getUserId()); // if (type.equals("")) { // List dataRepositoryList = (this.apiContext.getOperationsContext().getDatabaseRepository().getDataRepositoryDao().getWithCriteria(criteria)).toList(); // list = dataRepositoryList.stream().map(item -> new FetcherReference().fromDataRepository(item)).collect(Collectors.toList()); // } // } // case Registries: { // RegistryCriteria criteria = new RegistryCriteria(); // if (!query.isEmpty()) criteria.setLike(query); // criteria.setCreationUserId(this.userScope.getUserId()); // if (type.equals("")) { // List registryList = (this.apiContext.getOperationsContext().getDatabaseRepository().getRegistryDao().getWithCriteria(criteria)).toList(); // list = registryList.stream().map(item -> new FetcherReference().fromRegistry(item)).collect(Collectors.toList()); // } // } // case Services: // { // ServiceCriteria criteria = new ServiceCriteria(); // // if (!query.isEmpty()) criteria.setLike(query); // criteria.setCreationUserId(this.userScope.getUserId()); // // if (type.equals("")) { // List serviceList = (this.apiContext.getOperationsContext().getDatabaseRepository().getServiceDao().getWithCriteria(criteria)).toList(); // list = serviceList.stream().map(item -> new FetcherReference().fromService(item)).collect(Collectors.toList()); // } // } // case Datasets:{ // // ExternalDatasetCriteria criteria = apiContext.getOperationsContext().getBuilderFactory().getBuilder(ExternalDatasetCriteriaBuilder.class).like(query).build(); // // criteria.setCreationUserId(this.userScope.getUserId()); // QueryableList items = apiContext.getOperationsContext().getDatabaseRepository().getExternalDatasetDao().getWithCriteria(criteria); // // list = items.select(item -> new FetcherReference().fromDataset(item)); // } // case Taxonomies: // case Publications: // case Licenses: // break; // } // // return list; // } @Override public List searchReferenceWithDefinition(ReferenceDefinitionSearchLookup lookup) throws HugeResultSetException, MyNotFoundException, InvalidApplicationException { ReferenceTypeQuery query = this.queryFactory.query(ReferenceTypeQuery.class).authorize(AuthorizationFlags.OwnerOrDmpAssociatedOrPermissionOrPublic).ids(lookup.getReferenceTypeId()); List datas = query.collectAs(lookup.getProject()); if (datas.size() != 1 ){ return null; } ReferenceTypeDefinitionEntity definition = this.xmlHandlingService.fromXmlSafe(ReferenceTypeDefinitionEntity.class, datas.get(0).getDefinition()); List> remoteRepos = this.getAll(definition.getSources(), lookup); return null; } private List> getAll ( List sources, ReferenceDefinitionSearchLookup lookup){ List> results = new LinkedList<>(); if (sources == null || sources.isEmpty()) { return results; } sources.sort(Comparator.comparing(ReferenceTypeSourceBaseConfigurationEntity::getOrdinal)); List apiSources = sources.stream().filter(x-> ReferenceTypeSourceType.API.equals(x.getType())).map(x-> (ReferenceTypeSourceExternalApiConfigurationEntity)x).toList(); apiSources.forEach(source -> { try { String auth = null; if (source.getAuth()!= null) { //auth = this.getAuthentication(source.getAuth()); } results.addAll(getAllApiResultsFromUrl(source.getUrl(), null, source.getResults(), source.getPaginationPath(), lookup, source.getLabel(), source.getKey(), source.getContentType(), source.getFirstPage(), source.getRequestBody(), source.getHttpMethod(), source.getFilterType(), source.getQueries(), auth)); } catch (Exception e) { logger.error(e.getLocalizedMessage(), e); } }); List staticSources = sources.stream().filter(x-> ReferenceTypeSourceType.STATIC.equals(x.getType())).map(x-> (ReferenceTypeSourceStaticOptionConfigurationEntity)x).toList(); staticSources.forEach(source -> { Map map = new HashMap<>(); source.getOptions().forEach(option -> { map.put(option.getCode(), option.getValue()); map.put("tag", source.getLabel()); map.put("key", source.getKey()); }); results.add(map); }); return results; } private String getAuthentication(AuthenticationConfigurationEntity authenticationConfiguration) { HttpMethod method = HttpMethod.valueOf(authenticationConfiguration.getAuthMethod().name()); Map response = this.client.method(method).uri(authenticationConfiguration.getAuthUrl()) .contentType(MediaType.APPLICATION_JSON) .bodyValue(this.parseBodyString(authenticationConfiguration.getAuthRequestBody())) .exchangeToMono(mono -> mono.bodyToMono(new ParameterizedTypeReference>() { })).block(); return authenticationConfiguration.getType() + " " + response.get(authenticationConfiguration.getAuthTokenPath()); } 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; } private List> getAllApiResultsFromUrl(String urlPath, FetchStrategy fetchStrategy, final ResultsConfigurationEntity resultsEntity, final String jsonPaginationPath, ReferenceDefinitionSearchLookup lookup, String label, String key, String contentType, String firstPage, String requestBody, ReferenceTypeExternalApiHTTPMethodType requestType, String filterType, List queries, String auth) throws Exception { Set pages = new HashSet<>(); String replacedUrlPath = replaceLookupFields(urlPath, lookup, firstPage, queries); String replacedUrlBody = replaceLookupFields(requestBody, lookup, firstPage, queries); ExternalRefernceResult externalRefernceResult = getResultsFromUrl(replacedUrlPath, resultsEntity, jsonPaginationPath, contentType, replacedUrlBody, requestType, auth); if(externalRefernceResult != null) { if (filterType != null && filterType.equals("local") && (lookup.getLike() != null && !lookup.getLike().isEmpty())) { externalRefernceResult.setResults(externalRefernceResult.getResults().stream() .filter(r -> r.get("name").toLowerCase().contains(lookup.getLike().toLowerCase())) .collect(Collectors.toList())); } if (fetchStrategy == FetchStrategy.FIRST) return externalRefernceResult.getResults().stream().peek(x -> x.put("tag", label)).peek(x -> x.put("key", key)).collect(Collectors.toList()); if (externalRefernceResult.getPagination() != null && externalRefernceResult.getPagination().get("pages") != null) //if has more pages, add them to the pages set for (int i = 2; i <= externalRefernceResult.getPagination().get("pages"); i++) pages.add(i); //Long maxResults = configLoader.getExternalUrls().getMaxresults(); Long maxResults = Long.valueOf(1000); if ((maxResults > 0) && (externalRefernceResult.getPagination().get("count") > maxResults)) throw new HugeResultSetException("The submitted search query " + lookup.getLike() + " is about to return " + externalRefernceResult.getPagination().get("count") + " results... Please submit a more detailed search query"); Optional optionalResults = pages.parallelStream() .map(page -> getResultsFromUrl(urlPath + "&page=" + page, resultsEntity, jsonPaginationPath, contentType, replacedUrlBody, requestType, auth)) .filter(Objects::nonNull) .reduce((result1, result2) -> { result1.getResults().addAll(result2.getResults()); return result1; }); ExternalRefernceResult remainingExternalRefernceResult = optionalResults.orElseGet(ExternalRefernceResult::new); remainingExternalRefernceResult.getResults().addAll(externalRefernceResult.getResults()); return remainingExternalRefernceResult.getResults().stream().peek(x -> x.put("tag", label)).peek(x -> x.put("key", key)).collect(Collectors.toList()); } else { return new LinkedList<>(); } } private String replaceLookupFields(String urlPath, ReferenceDefinitionSearchLookup lookup, String firstPage, List queries){ String completedPath = urlPath; if (urlPath.contains("openaire") || urlPath.contains("orcid") ){ if (lookup.getLike() != null) { completedPath = completedPath.replace("{like}", lookup.getLike()); } else { completedPath = completedPath.replace("{like}", "*"); } } if (urlPath.contains("{like}")){ if (lookup.getLike() != null) { completedPath = completedPath.replace("{like}", lookup.getLike()); } else { completedPath = completedPath.replace("{like}", ""); } } if (urlPath.contains("{page}")) { if (lookup.getPage() != null && lookup.getPage().getOffset() > 0) { completedPath = completedPath.replace("{page}", String.valueOf(lookup.getPage().getOffset())); } else if (firstPage != null) { completedPath = completedPath.replace("{page}", firstPage); } else { completedPath = completedPath.replace("{page}", "1"); } } if (urlPath.contains("{pageSize}")){ if (lookup.getPage() != null && lookup.getPage().getSize() > 0) { completedPath = completedPath.replace("{pageSize}", String.valueOf(lookup.getPage().getSize())); } else { completedPath = completedPath.replace("{pageSize}", "100"); } } return completedPath; } protected ExternalRefernceResult getResultsFromUrl(String urlString, ResultsConfigurationEntity resultsEntity, String jsonPaginationPath, String contentType, String requestBody, ReferenceTypeExternalApiHTTPMethodType httpMethod, String auth) { try { ResponseEntity response; JsonNode jsonBody = new ObjectMapper().readTree(requestBody); response = this.client.method(HttpMethod.valueOf(httpMethod.name())).uri(urlString).headers(httpHeaders -> { if (contentType != null && !contentType.isEmpty()) { httpHeaders.setAccept(Collections.singletonList(MediaType.valueOf(contentType))); httpHeaders.setContentType(MediaType.valueOf(contentType)); } if (auth != null) { httpHeaders.set("Authorization", auth); } }).bodyValue(jsonBody).retrieve().toEntity(String.class).block(); if (response.getStatusCode() == HttpStatus.OK) { // success ExternalRefernceResult externalRefernceResult = new ExternalRefernceResult(); if (response.getHeaders().get("Content-Type").get(0).contains("json")) { DocumentContext jsonContext = JsonPath.parse(response.getBody()); externalRefernceResult = this.getFromJson(jsonContext, resultsEntity); } if (externalRefernceResult.getPagination().size() == 0) { externalRefernceResult.getPagination().put("pages", 1); externalRefernceResult.getPagination().put("count", externalRefernceResult.getResults().size()); } return externalRefernceResult; } } catch (Exception exception) { logger.error(exception.getMessage(), exception); } return null; } public static ExternalRefernceResult getFromJson(DocumentContext jsonContext, ResultsConfigurationEntity resultsEntity) { return new ExternalRefernceResult(parseData(jsonContext, resultsEntity), new HashMap<>(1, 1)); } private static List> parseData (DocumentContext jsonContext, ResultsConfigurationEntity resultsEntity) { List > rawData = jsonContext.read(resultsEntity.getResultsArrayPath()); List> parsedData = new ArrayList<>(); for (Map stringObjectMap: rawData){ Map map = new HashMap<>(); for(ResultFieldsMappingConfigurationEntity field: resultsEntity.getFieldsMapping()){ String pathValue = field.getResponsePath(); if (!pathValue.contains(".")){ if (stringObjectMap.containsKey(pathValue)) { //map.put(field.getCode(), stringObjectMap.get(pathValue)); map.put(field.getCode(), normalizeValue(stringObjectMap.get(pathValue))); } }else { if (stringObjectMap.containsKey(pathValue.split("\\.")[0])){ String value = null; Object fieldObj = stringObjectMap.get(pathValue.split("\\.")[0]); if (fieldObj != null){ if (fieldObj instanceof Map){ Object o = ((Map) fieldObj).get(pathValue.split("\\.")[1]); if(o instanceof String){ value = (String)o; } else if(o instanceof Integer){ value = String.valueOf(o); } } else if (fieldObj instanceof List) { Object o = ((List>) fieldObj).get(0).get(pathValue.split("\\.")[1]); if(o instanceof String){ value = (String)o; } else if(o instanceof Integer){ value = String.valueOf(o); } } } if (value != null){ map.put(field.getCode(), value); } } } } parsedData.add(map); } return parsedData; } private static String normalizeValue(Object value) { if (value instanceof JSONArray) { JSONArray jarr = (JSONArray) value; if (jarr.get(0) instanceof String) { return jarr.get(0).toString(); } else { for (Object o : jarr) { if ((o instanceof Map) && ((Map) o).containsKey("content")) { try { return ((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 ((Map)value).get(key); } return value != null ? value.toString() : null; } }