diff --git a/.gitignore b/.gitignore index 9154f4c..9738e7c 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,8 @@ # Compiled class file *.class +.idea +target # Log file *.log diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..e64ccfd --- /dev/null +++ b/pom.xml @@ -0,0 +1,93 @@ + + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 3.3.0 + + + eu.dnetlib + scholexplorer-api + 0.0.1-SNAPSHOT + scholexplorer-api + Demo project for Spring Boot + + 17 + + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-actuator + + + io.micrometer + micrometer-registry-prometheus + runtime + + + org.springframework + spring-aspects + + + org.apache.commons + commons-pool2 + 2.11.1 + + + javax.annotation + javax.annotation-api + 1.3.2 + + + eu.dnetlib.dhp + dhp-schemas + 6.1.3-FLAT-SCHOLIX + + + org.springframework.data + spring-data-elasticsearch + 4.2.2 + + + org.elasticsearch.client + elasticsearch-rest-high-level-client + 7.6.2 + + + org.springframework.boot + spring-boot-starter-test + test + + + org.springdoc + springdoc-openapi-starter-webmvc-ui + 2.5.0 + + + io.swagger.core.v3 + swagger-annotations + 2.2.22 + + + + + + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + diff --git a/src/main/java/eu/dnetlib/scholexplorer/api/RestClientConfig.java b/src/main/java/eu/dnetlib/scholexplorer/api/RestClientConfig.java new file mode 100644 index 0000000..4d2b49a --- /dev/null +++ b/src/main/java/eu/dnetlib/scholexplorer/api/RestClientConfig.java @@ -0,0 +1,26 @@ +package eu.dnetlib.scholexplorer.api; + + +import eu.dnetlib.scholexplorer.api.index.ElasticSearchPool; +import eu.dnetlib.scholexplorer.api.index.ElasticSearchProperties; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class RestClientConfig { + + @Autowired + private ElasticSearchProperties elasticSearchProperties; + + + @Bean + public ElasticSearchPool connectionPool() { + + elasticSearchProperties.setMaxIdle(5); + elasticSearchProperties.setMaxTotal(10); + return new ElasticSearchPool(elasticSearchProperties); + } + + +} diff --git a/src/main/java/eu/dnetlib/scholexplorer/api/ScholexplorerApiApplication.java b/src/main/java/eu/dnetlib/scholexplorer/api/ScholexplorerApiApplication.java new file mode 100644 index 0000000..7fb87a7 --- /dev/null +++ b/src/main/java/eu/dnetlib/scholexplorer/api/ScholexplorerApiApplication.java @@ -0,0 +1,122 @@ +package eu.dnetlib.scholexplorer.api; + +import io.micrometer.core.aop.TimedAspect; +import io.micrometer.core.instrument.Meter; +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.config.MeterFilter; +import io.micrometer.core.instrument.distribution.DistributionStatisticConfig; +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.info.License; +import org.apache.commons.lang3.StringUtils; +import org.springdoc.core.models.GroupedOpenApi; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.Bean; +import io.swagger.v3.oas.models.servers.Server; +import io.swagger.v3.oas.models.info.Info; + +import javax.annotation.PostConstruct; +import java.util.ArrayList; +import java.util.List; + +@SpringBootApplication +public class ScholexplorerApiApplication { + + @Value("${server.public_url}") + private String serverPublicUrl; + + @Value("${server.public_desc}") + private String serverPublicDesc; + + protected static final License AGPL_3_LICENSE = + new License().name("GNU Affero General Public License v3.0 or later").url("https://www.gnu.org/licenses/agpl-3.0.txt"); + + private final double scale = 1000000000; + + private final double[] histogramValues = new double[] { + .005 * scale, .01 * scale, .25 * scale, .5 * scale, .75 * scale, scale, 2.5 * scale, 5.0 * scale, 7.5 * scale, 10.0 * scale + }; + + @Bean + public TimedAspect timedAspect(final MeterRegistry meterRegistry) { + final MeterFilter mf = new MeterFilter() { + + @Override + public DistributionStatisticConfig configure(final Meter.Id id, final DistributionStatisticConfig config) { + if (id.getName().startsWith(ScholixAPIConstants.SCHOLIX_COUNTER_PREFIX)) { + + return DistributionStatisticConfig.builder() + .percentilesHistogram(false) + .serviceLevelObjectives(histogramValues) + .build() + .merge(config); + } + return config; + } + }; + meterRegistry.config().meterFilter(mf); + return new TimedAspect(meterRegistry); + } + + + @Bean + public TaggedCounter myCounter(final MeterRegistry meterRegistry) { + + return new TaggedCounter(ScholixAPIConstants.SCHOLIX_MANAGER_COUNTER_NAME, ScholixAPIConstants.SCHOLIX_MANAGER_TAG_NAME, meterRegistry); + } + + @Bean + public GroupedOpenApi publicApiV1() { + return GroupedOpenApi.builder() + .group(ScholixAPIConstants.API_V1_NAME) + .pathsToMatch("/v1/**") + .build(); + } + + @Bean + public GroupedOpenApi publicApiV2() { + return GroupedOpenApi.builder() + .group(ScholixAPIConstants.API_V2_NAME) + .pathsToMatch("/v2/**") + .build(); + } + + + @Bean + public OpenAPI newSwaggerDocket() { + final List servers = new ArrayList<>(); + if (StringUtils.isNotBlank(serverPublicUrl)) { + final Server server = new Server(); + server.setUrl(serverPublicUrl); + server.setDescription(serverPublicDesc); + servers.add(server); + } + + return new OpenAPI() + .servers(servers) + .info(getSwaggerInfo()) + .tags(new ArrayList<>()); + } + + private Info getSwaggerInfo() { + return new Info() + .title(swaggerTitle()) + .description(swaggerDesc()) + .version("1.0") + .license(AGPL_3_LICENSE); + } + + protected String swaggerTitle() { + return "ScholeExplorer APIs"; + } + + protected String swaggerDesc() { + return ScholixAPIConstants.API_DESCRIPTION; + } + + public static void main(String[] args) { + SpringApplication.run(ScholexplorerApiApplication.class, args); + } + +} diff --git a/src/main/java/eu/dnetlib/scholexplorer/api/ScholixAPIConstants.java b/src/main/java/eu/dnetlib/scholexplorer/api/ScholixAPIConstants.java new file mode 100644 index 0000000..94d6773 --- /dev/null +++ b/src/main/java/eu/dnetlib/scholexplorer/api/ScholixAPIConstants.java @@ -0,0 +1,23 @@ +package eu.dnetlib.scholexplorer.api; + +public class ScholixAPIConstants { + + public static final String API_V1_NAME = "Scholexplorer API V1.0"; + public static final String API_V2_NAME = "Scholexplorer API V2.0"; + + public static String API_DESCRIPTION ="

\"ScholeXplorer\"

" + + "The Scholix Swagger API allows clients to run REST queries over the Scholexplorer index in order to fetch links matching given criteria. In the current version, clients can search for:" + + ""; + + + public static String SCHOLIX_MANAGER_COUNTER_NAME= "scholixLinkCounter"; + public static final String SCHOLIX_MANAGER_TAG_NAME = "links"; + + public static String SCHOLIX_COUNTER_PREFIX = "scholix"; + + + + +} diff --git a/src/main/java/eu/dnetlib/scholexplorer/api/ScholixException.java b/src/main/java/eu/dnetlib/scholexplorer/api/ScholixException.java new file mode 100644 index 0000000..b59a4f8 --- /dev/null +++ b/src/main/java/eu/dnetlib/scholexplorer/api/ScholixException.java @@ -0,0 +1,35 @@ +package eu.dnetlib.scholexplorer.api; + + +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.ResponseStatus; + +@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) +public class ScholixException extends Exception{ + + private static final long serialVersionUID = -3414428892721711308L; + + + public ScholixException() { + super(); + } + + public ScholixException(String message, Throwable cause, boolean enableSuppression, + boolean writableStackTrace) { + super(message, cause, enableSuppression, writableStackTrace); + } + + public ScholixException(String message, Throwable cause) { + super(message, cause); + } + + public ScholixException(String message) { + super(message); + } + + public ScholixException(Throwable cause) { + super(cause); + } + + +} \ No newline at end of file diff --git a/src/main/java/eu/dnetlib/scholexplorer/api/TaggedCounter.java b/src/main/java/eu/dnetlib/scholexplorer/api/TaggedCounter.java new file mode 100644 index 0000000..088dd90 --- /dev/null +++ b/src/main/java/eu/dnetlib/scholexplorer/api/TaggedCounter.java @@ -0,0 +1,33 @@ +package eu.dnetlib.scholexplorer.api; + +import io.micrometer.core.instrument.Counter; + +import io.micrometer.core.instrument.MeterRegistry; +import java.util.HashMap; +import java.util.Map; + + +public class TaggedCounter { + + private final String name; + private final String tagName; + private final MeterRegistry registry; + private final Map counters = new HashMap<>(); + + + public TaggedCounter(String name, String tagName, MeterRegistry registry) { + this.name = name; + this.tagName = tagName; + this.registry = registry; + } + + + public void increment(String tagValue){ + Counter counter = counters.get(tagValue); + if(counter == null) { + counter = Counter.builder(name).tags(tagName, tagValue).register(registry); + counters.put(tagValue, counter); + } + counter.increment(); + } +} \ No newline at end of file diff --git a/src/main/java/eu/dnetlib/scholexplorer/api/controller/DatasourceV1.java b/src/main/java/eu/dnetlib/scholexplorer/api/controller/DatasourceV1.java new file mode 100644 index 0000000..3510f9f --- /dev/null +++ b/src/main/java/eu/dnetlib/scholexplorer/api/controller/DatasourceV1.java @@ -0,0 +1,39 @@ +package eu.dnetlib.scholexplorer.api.controller; + +import eu.dnetlib.dhp.schema.sx.api.model.v1.LinkPublisher; +import eu.dnetlib.scholexplorer.api.ScholixException; +import eu.dnetlib.scholexplorer.api.index.ScholixIndexManager; +import io.micrometer.core.annotation.Timed; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.apache.commons.lang3.tuple.Pair; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +@RestController +@RequestMapping("/v1") +@Tag(name = "Datasources") +public class DatasourceV1 { + + @Autowired + private ScholixIndexManager manager; + + @Timed(value = "scholix.v1.datasources", description = "Time taken to return all datasources on Version 1.0 of Scholix") + @Operation(summary = "Get all Datasources", description = "returns a list of all datasources") + @GetMapping("/listDatasources") + public List getDatasources() throws ScholixException { + + final List> result = manager.totalLinksByProvider(null); + + if (result == null) { return new ArrayList<>(); } + + return result.stream().map(p -> new LinkPublisher().name(p.getKey()).totalRelationships(p.getValue().intValue())).collect(Collectors.toList()); + + } +} diff --git a/src/main/java/eu/dnetlib/scholexplorer/api/controller/HomeController.java b/src/main/java/eu/dnetlib/scholexplorer/api/controller/HomeController.java new file mode 100644 index 0000000..2699e63 --- /dev/null +++ b/src/main/java/eu/dnetlib/scholexplorer/api/controller/HomeController.java @@ -0,0 +1,36 @@ +package eu.dnetlib.scholexplorer.api.controller; + + +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.GetMapping; + +@Controller +public class HomeController { + + @GetMapping({ + "/doc", "/swagger" + }) + public String apiDoc() { + return "redirect:swagger-ui/index.html"; + } + + @GetMapping({ + "/v1/ui" + }) + public String v1Doc() { + return "redirect:/swagger-ui/index.html?urls.primaryName=Scholexplorer%20API%20V1.0"; + } + + + @GetMapping({ + "/v2/ui" + }) + public String v2Doc() { + return "redirect:/swagger-ui/index.html?urls.primaryName=Scholexplorer%20API%20V2.0"; + } + + + + + +} diff --git a/src/main/java/eu/dnetlib/scholexplorer/api/controller/LinkProviderV2.java b/src/main/java/eu/dnetlib/scholexplorer/api/controller/LinkProviderV2.java new file mode 100644 index 0000000..233ea7a --- /dev/null +++ b/src/main/java/eu/dnetlib/scholexplorer/api/controller/LinkProviderV2.java @@ -0,0 +1,42 @@ +package eu.dnetlib.scholexplorer.api.controller; + +import eu.dnetlib.dhp.schema.sx.api.model.v2.LinkProviderType; +import eu.dnetlib.scholexplorer.api.ScholixException; +import eu.dnetlib.scholexplorer.api.index.ScholixIndexManager; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.enums.ParameterIn; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.apache.commons.lang3.tuple.Pair; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +@RestController +@RequestMapping("/v2") +@Tag(name = "LinkProvider : Operation related to the Link Provider") +public class LinkProviderV2 { + + @Autowired + ScholixIndexManager manager; + + @Operation(summary = "Get all Link Providers", description = "Return a list of link provider and relative number of relations") + @GetMapping("/LinkProvider") + public List getLinkProviders( + @Parameter(in = ParameterIn.QUERY, description = "Filter the link provider name") @RequestParam(required = false) final String name) + throws ScholixException { + + final List> result = manager.totalLinksByProvider(name); + + if (result == null) { return new ArrayList<>(); } + + return result.stream().map(s -> new LinkProviderType().name(s.getLeft()).totalRelationships(s.getValue().intValue())).collect(Collectors.toList()); + + } +} diff --git a/src/main/java/eu/dnetlib/scholexplorer/api/controller/LinkPublisherV2.java b/src/main/java/eu/dnetlib/scholexplorer/api/controller/LinkPublisherV2.java new file mode 100644 index 0000000..e52a7d7 --- /dev/null +++ b/src/main/java/eu/dnetlib/scholexplorer/api/controller/LinkPublisherV2.java @@ -0,0 +1,56 @@ +package eu.dnetlib.scholexplorer.api.controller; + +import eu.dnetlib.dhp.schema.sx.api.model.v2.LinkProviderType; +import eu.dnetlib.scholexplorer.api.ScholixException; +import eu.dnetlib.scholexplorer.api.index.ScholixIndexManager; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.enums.ParameterIn; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.apache.commons.lang3.tuple.Pair; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +@RestController +@RequestMapping("/v2/LinkPublisher") +@Tag(name = "LinkPublisher : Operation related to the Link Publisher") +public class LinkPublisherV2 { + + @Autowired + ScholixIndexManager manager; + + @Operation(summary = "Get All Publishers that provide source object", description = "Return a List of all Publishers that provide source objects in Scholix " + + + "links and the total number of links where the source object comes from this publisher") + @GetMapping("/inSource") + public List getInSource( + @Parameter(in = ParameterIn.QUERY, description = "Filter the link publisher name") @RequestParam(required = false) final String name) + throws ScholixException { + final List> result = manager.totalLinksPublisher(ScholixIndexManager.RelationPrefix.source, name); + + if (result == null) { return new ArrayList<>(); } + + return result.stream().map(s -> new LinkProviderType().name(s.getLeft()).totalRelationships(s.getValue().intValue())).collect(Collectors.toList()); + } + + @Operation(summary = "Get All Publishers that provide target object", description = "Return a List of all Publishers that provide source objects in Scholix " + + + "links and the total number of links where the target object comes from this publisher") + @GetMapping("/inTarget") + public List getInTarget( + @Parameter(in = ParameterIn.QUERY, description = "Filter the link publisher name") @RequestParam(required = false) final String name) + throws ScholixException { + final List> result = manager.totalLinksPublisher(ScholixIndexManager.RelationPrefix.target, name); + + if (result == null) { return new ArrayList<>(); } + + return result.stream().map(s -> new LinkProviderType().name(s.getLeft()).totalRelationships(s.getValue().intValue())).collect(Collectors.toList()); + } +} diff --git a/src/main/java/eu/dnetlib/scholexplorer/api/controller/ScholixControllerV2.java b/src/main/java/eu/dnetlib/scholexplorer/api/controller/ScholixControllerV2.java new file mode 100644 index 0000000..e2236f1 --- /dev/null +++ b/src/main/java/eu/dnetlib/scholexplorer/api/controller/ScholixControllerV2.java @@ -0,0 +1,78 @@ +package eu.dnetlib.scholexplorer.api.controller; + +import eu.dnetlib.dhp.schema.sx.api.model.v2.PageResultType; +import eu.dnetlib.dhp.schema.sx.api.model.v2.ScholixType; +import eu.dnetlib.dhp.schema.sx.scholix.Scholix; +import eu.dnetlib.scholexplorer.api.ScholixException; +import eu.dnetlib.scholexplorer.api.index.ScholixIndexManager; +import io.micrometer.core.annotation.Timed; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.enums.ParameterIn; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.tuple.Pair; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; +import java.util.stream.Collectors; + +@RestController +@RequestMapping("/v2") +@Tag(name = "Links : Operation related to the Scholix Links") +public class ScholixControllerV2 { + + @Autowired + ScholixIndexManager repository; + + + @Timed(value = "scholix.v2.links", description = "Time taken to return links on Version 2.0 of Scholix") + @Operation(summary = "Get Scholix Links") + @GetMapping("/Links") + public PageResultType links( + @RequestParam(required = false) + @Parameter(in = ParameterIn.QUERY, description = "Filter Scholix relationships collected from a LinkProvider") final String linkProvider, + @RequestParam(required = false) + @Parameter(in = ParameterIn.QUERY, description = "Filter Scholix relationships having a target pid") final String targetPid, + @RequestParam(required = false) + @Parameter(in = ParameterIn.QUERY, description = "Filter Scholix relationships having a target pid type") final String targetPidType, + @RequestParam(required = false) + @Parameter(in = ParameterIn.QUERY, description = "Filter Scholix relationships having a target published in a Publisher named targetPublisher") final String targetPublisher, + @RequestParam(required = false) + @Parameter(in = ParameterIn.QUERY, description = "Filter Scholix relationships having a target type (literature, dataset, unknown)") final String targetType, + @RequestParam(required = false) + @Parameter(in = ParameterIn.QUERY, description = "Filter Scholix relationships having a source pid") final String sourcePid, + @RequestParam(required = false) + @Parameter(in = ParameterIn.QUERY, description = "Filter Scholix relationships having a source pid type") final String sourcePidType, + @RequestParam(required = false) + @Parameter(in = ParameterIn.QUERY, description = "Filter Scholix relationships having a source published in a Publisher named sourcePublisher") final String sourcePublisher, + @RequestParam(required = false) + @Parameter(in = ParameterIn.QUERY, description = "Filter Scholix relationships having a source type (literature, dataset, unknown)") final String sourceType, + // @Parameter(in = ParameterIn.QUERY, + // description = "Filter scholix Links having collected after this date") String harvestedAfter, + @Parameter(in = ParameterIn.QUERY, description = "select page of result") final Integer page) throws Exception { + + if (StringUtils.isEmpty(sourcePid) && StringUtils.isEmpty(targetPid) && StringUtils.isEmpty(sourcePublisher) && StringUtils.isEmpty(targetPublisher)&&StringUtils.isEmpty(sourceType) + && StringUtils.isEmpty(linkProvider)) { + throw new ScholixException( + "The method requires one of the following parameters: sourcePid, targetPid, sourcePublisher, targetPublisher, linkProvider, sourceType"); + } + + try { + final int currentPage = page != null ? page : 0; + final Pair> scholixResult = repository + .linksFromPid(linkProvider, targetPid, targetPidType, targetPublisher, targetType, sourcePid, sourcePidType, sourcePublisher, sourceType, null,currentPage); + final PageResultType pageResult = new PageResultType(); + pageResult.setTotalPages(scholixResult.getLeft().intValue() / 10); + pageResult.setTotalLinks(scholixResult.getLeft().intValue()); + pageResult.setResult(scholixResult.getRight().stream().map(ScholixType::fromScholix).collect(Collectors.toList())); + return pageResult; + } catch (final Throwable e) { + throw new ScholixException("Error on requesting url ", e); + } + } +} diff --git a/src/main/java/eu/dnetlib/scholexplorer/api/controller/ScholixLinkControllerV1.java b/src/main/java/eu/dnetlib/scholexplorer/api/controller/ScholixLinkControllerV1.java new file mode 100644 index 0000000..9d7bffd --- /dev/null +++ b/src/main/java/eu/dnetlib/scholexplorer/api/controller/ScholixLinkControllerV1.java @@ -0,0 +1,81 @@ +package eu.dnetlib.scholexplorer.api.controller; + + +import eu.dnetlib.dhp.schema.sx.api.model.v1.ScholixV1; +import eu.dnetlib.dhp.schema.sx.scholix.Scholix; +import eu.dnetlib.scholexplorer.api.ScholixException; +import eu.dnetlib.scholexplorer.api.index.ScholixIndexManager; +import io.micrometer.core.annotation.Timed; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.enums.ParameterIn; +import org.apache.commons.lang3.tuple.Pair; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import javax.validation.constraints.NotNull; +import java.util.List; +import java.util.stream.Collectors; + +@RestController +@RequestMapping("/v1") +public class ScholixLinkControllerV1 { + + @Autowired + ScholixIndexManager repository; + + @Operation(summary = "Get all Scholix relation collected from a publisher", description = "return a list of scholix object published from a specific publisher") + @GetMapping("/linksFromPublisher") + @Timed(value = "scholix.v1.linksFromPublisher", description = "Time taken to return links on Version 1.0 of Scholix collected from a publisher") + public List linksFromPublisher( + + @Parameter(in = ParameterIn.QUERY, description = "Filter Scholix relationships collected from a publisher", required = true) final String publisher, + @Parameter(in = ParameterIn.QUERY, description = "The page number") @RequestParam(required = false) final Integer page) throws ScholixException { + + final int currentPage = page != null ? page : 0; + + final Pair> scholixResult = repository.linksFromPid(null, null, null, publisher, null, null, null, null, null,null, currentPage); + final List scholixData = scholixResult.getValue(); + if (scholixData == null) { return null; } + return scholixData.stream().map(ScholixV1::fromScholix).collect(Collectors.toList()); + } + + @Operation(summary = "Get all Scholix relation collected from a datasource", description = "return a list of scholix object collected from a specific datasource") + @GetMapping("/linksFromDatasource") + @Timed(value = "scholix.v1.linksFromDatasource", description = "Time taken to return links on Version 1.0 of Scholix collected from a LinkProvider") + public List linksFromDatasource( + @Parameter(in = ParameterIn.QUERY, description = "Filter Scholix relationships collected from a LinkProvider") @NotNull final String datasource, + @Parameter(in = ParameterIn.QUERY, description = "The page number") @RequestParam(required = false) final Integer page) throws ScholixException { + + final int currentPage = page != null ? page : 0; + final Pair> scholixResult = repository.linksFromPid(datasource, null, null, null, null, null, null, null, null, null,currentPage); + final List scholixData = scholixResult.getValue(); + if (scholixData == null) { return null; } + return scholixData.stream().map(ScholixV1::fromScholix).collect(Collectors.toList()); + } + + + + @Operation(summary = "Retrieve all scholix links from a persistent identifier", description = "The linksFromPid endpoint returns a list of scholix object related from a specific persistent identifier") + @GetMapping("/linksFromPid") + @Timed(value = "scholix.v1.linksFromPid", description = "Time taken to return links on Version 1.0 of Scholix related from a specific persistent identifier") + public List linksFromPid( + @Parameter(in = ParameterIn.QUERY, description = "persistent Identifier") @NotNull final String pid, + @Parameter(in = ParameterIn.QUERY, description = "Persistent Identifier Type") @RequestParam(required = false) final String pidType, + @Parameter(in = ParameterIn.QUERY, description = "typology target filter should be publication, dataset or unknown") @RequestParam(required = false) final String typologyTarget, + @Parameter(in = ParameterIn.QUERY, description = "a datasource provenance filter of the target relation") @RequestParam(required = false) final String datasourceTarget, + @Parameter(in = ParameterIn.QUERY, description = "The page number") @RequestParam(required = false) final Integer page) throws ScholixException { + + final int currentPage = page != null ? page : 0; + final Pair> scholixResult = + repository.linksFromPid(datasourceTarget, null, null, null, typologyTarget, pid, pidType, null, null, null, currentPage); + final List scholixData = scholixResult.getValue(); + if (scholixData == null) { return null; } + return scholixData.stream().map(ScholixV1::fromScholix).collect(Collectors.toList()); + } + + +} diff --git a/src/main/java/eu/dnetlib/scholexplorer/api/index/ElasticSearchClientFactory.java b/src/main/java/eu/dnetlib/scholexplorer/api/index/ElasticSearchClientFactory.java new file mode 100644 index 0000000..ceb9fff --- /dev/null +++ b/src/main/java/eu/dnetlib/scholexplorer/api/index/ElasticSearchClientFactory.java @@ -0,0 +1,74 @@ +package eu.dnetlib.scholexplorer.api.index; + +import org.apache.commons.lang3.tuple.ImmutablePair; +import org.apache.commons.lang3.tuple.Pair; +import org.apache.commons.pool2.PooledObject; +import org.apache.commons.pool2.PooledObjectFactory; +import org.apache.commons.pool2.impl.DefaultPooledObject; +import org.elasticsearch.client.RequestOptions; +import org.elasticsearch.client.RestHighLevelClient; +import org.springframework.data.elasticsearch.client.ClientConfiguration; +import org.springframework.data.elasticsearch.client.RestClients; +import org.springframework.data.elasticsearch.core.ElasticsearchRestTemplate; + +/** + * The type Elastic search client factory. + */ +public class ElasticSearchClientFactory implements PooledObjectFactory> { + + private final ElasticSearchProperties elasticSearchProperties; + + + /** + * Instantiates a new Elastic search client factory. + * + * @param elasticSearchProperties the elastic search properties + */ + public ElasticSearchClientFactory(final ElasticSearchProperties elasticSearchProperties){ + this.elasticSearchProperties = elasticSearchProperties; + + } + + public PooledObject> makeObject() throws Exception { + + final ClientConfiguration clientConfiguration = ClientConfiguration.builder() + .connectedTo(elasticSearchProperties.getClusterNodes().split(",")) + .withConnectTimeout(elasticSearchProperties.getConnectionTimeout()) + .withSocketTimeout(elasticSearchProperties.getSocketTimeout()) + .build(); + RestHighLevelClient cc = RestClients.create(clientConfiguration).rest(); + + return new DefaultPooledObject<>(new ImmutablePair<>(cc, new ElasticsearchRestTemplate(cc))); + } + + public void destroyObject(PooledObject> pooledObject) throws Exception { + RestHighLevelClient client = pooledObject.getObject().getLeft(); + if(client!=null&&client.ping(RequestOptions.DEFAULT)){ + try { + client.close(); + }catch (Exception e){ + //ignore + } + } + } + + public boolean validateObject(PooledObject> pooledObject) { + RestHighLevelClient client = pooledObject.getObject().getLeft(); + try { + return client.ping(RequestOptions.DEFAULT); + }catch(Exception e){ + return false; + } + } + + public void activateObject(PooledObject> pooledObject) throws Exception { + RestHighLevelClient client = pooledObject.getObject().getLeft(); + boolean response = client.ping(RequestOptions.DEFAULT); + } + + public void passivateObject(PooledObject> pooledObject) throws Exception { + //nothing + } + + +} \ No newline at end of file diff --git a/src/main/java/eu/dnetlib/scholexplorer/api/index/ElasticSearchPool.java b/src/main/java/eu/dnetlib/scholexplorer/api/index/ElasticSearchPool.java new file mode 100644 index 0000000..9fccd3e --- /dev/null +++ b/src/main/java/eu/dnetlib/scholexplorer/api/index/ElasticSearchPool.java @@ -0,0 +1,32 @@ +package eu.dnetlib.scholexplorer.api.index; + +import org.apache.commons.lang3.tuple.Pair; +import org.elasticsearch.client.RestHighLevelClient; +import org.springframework.data.elasticsearch.core.ElasticsearchRestTemplate; + +/** + * The type Elastic search pool. + */ +public class ElasticSearchPool extends Pool> { + + private final ElasticSearchProperties elasticSearchProperties; + + /** + * Instantiates a new Elastic search pool. + * + * @param elasticSearchProperties the elastic search properties + */ + public ElasticSearchPool(ElasticSearchProperties elasticSearchProperties){ + super(elasticSearchProperties, new ElasticSearchClientFactory(elasticSearchProperties)); + this.elasticSearchProperties = elasticSearchProperties; + } + + /** + * Gets elastic search properties. + * + * @return the elastic search properties + */ + public ElasticSearchProperties getElasticSearchProperties() { + return elasticSearchProperties; + } +} \ No newline at end of file diff --git a/src/main/java/eu/dnetlib/scholexplorer/api/index/ElasticSearchProperties.java b/src/main/java/eu/dnetlib/scholexplorer/api/index/ElasticSearchProperties.java new file mode 100644 index 0000000..9a9beec --- /dev/null +++ b/src/main/java/eu/dnetlib/scholexplorer/api/index/ElasticSearchProperties.java @@ -0,0 +1,114 @@ +package eu.dnetlib.scholexplorer.api.index; + +import org.apache.commons.pool2.impl.GenericObjectPoolConfig; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +import javax.validation.constraints.NotNull; + +/** + * The type Elastic search properties. + */ +@Component("elasticSearchProperties") +@ConfigurationProperties(prefix = "scholix.elastic") +public class ElasticSearchProperties extends GenericObjectPoolConfig { + + @NotNull + private String clusterNodes; + @NotNull + private String indexName; + @NotNull + private String indexResourceName; + @NotNull + private long connectionTimeout; + @NotNull + private long socketTimeout; + + /** + * Gets cluster nodes. + * + * @return the cluster nodes + */ + public String getClusterNodes() { + return clusterNodes; + } + + /** + * Sets cluster nodes. + * + * @param clusterNodes the cluster nodes + * @return the cluster nodes + */ + public ElasticSearchProperties setClusterNodes(String clusterNodes) { + this.clusterNodes = clusterNodes; + return this; + } + + /** + * Gets index name. + * + * @return the index name + */ + public String getIndexName() { + return indexName; + } + + /** + * Sets index name. + * + * @param indexName the index name + * @return the index name + */ + public ElasticSearchProperties setIndexName(String indexName) { + this.indexName = indexName; + return this; + } + + public String getIndexResourceName() { + return indexResourceName; + } + + public void setIndexResourceName(String indexResourceName) { + this.indexResourceName = indexResourceName; + } + + /** + * Gets connection timeout. + * + * @return the connection timeout + */ + public long getConnectionTimeout() { + return connectionTimeout; + } + + /** + * Sets connection timeout. + * + * @param connectionTimeout the connection timeout + * @return the connection timeout + */ + public ElasticSearchProperties setConnectionTimeout(long connectionTimeout) { + this.connectionTimeout = connectionTimeout; + return this; + } + + /** + * Gets socket timeout. + * + * @return the socket timeout + */ + public long getSocketTimeout() { + return socketTimeout; + } + + /** + * Sets socket timeout. + * + * @param socketTimeout the socket timeout + * @return the socket timeout + */ + public ElasticSearchProperties setSocketTimeout(long socketTimeout) { + this.socketTimeout = socketTimeout; + return this; + } +} diff --git a/src/main/java/eu/dnetlib/scholexplorer/api/index/Pool.java b/src/main/java/eu/dnetlib/scholexplorer/api/index/Pool.java new file mode 100644 index 0000000..78f1c82 --- /dev/null +++ b/src/main/java/eu/dnetlib/scholexplorer/api/index/Pool.java @@ -0,0 +1,232 @@ +package eu.dnetlib.scholexplorer.api.index; + + +import eu.dnetlib.scholexplorer.api.ScholixException; +import org.apache.commons.pool2.PooledObjectFactory; +import org.apache.commons.pool2.impl.GenericObjectPool; +import org.apache.commons.pool2.impl.GenericObjectPoolConfig; + +/** + * When using the Java High Level REST Client provided by the Elasticsearch official website, it is found that there is no in the client API. + * Connecting to connect the pool, create a new connection every time, this is impact in high concurrency situation, so it is ready to be on the client + * API increases the concept of pool. + * + * Fortunately, we don't need to turn your weight to write the implementation of the connection pool, because Apache provides us with the general framework of the connection pool. + * Commons-pool2, and we only need to implement some logic according to the frame design. Used in the REDIS client API + * Jedispool is based on Commons-pool2 implementation. + * + * Let's take a look at how to achieve it. + * + * First we have to create a pool class, this pool introduces GenericObjectPool in Commons-pool2 through dependent manner. In this class + * In, we define how to borrow objects and returns objects from the pool. + * + * @param the type parameter + */ +public class Pool implements Cloneable { + + /** + * The Internal pool. + */ + protected GenericObjectPool internalPool ; + + /** + * Instantiates a new Pool. + */ + public Pool(){ + super(); + } + + /** + * Instantiates a new Pool. + * + * @param poolConfig the pool config + * @param factory the factory + */ + public Pool(final GenericObjectPoolConfig poolConfig, PooledObjectFactory factory){ + initPool(poolConfig, factory); + } + + /** + * Init pool. + * + * @param poolConfig the pool config + * @param factory the factory + */ + public void initPool(final GenericObjectPoolConfig poolConfig, PooledObjectFactory factory) { + + if (this.internalPool != null) { + try { + closeInternalPool(); + } catch (Exception e) { + } + } + + this.internalPool = new GenericObjectPool(factory, poolConfig); + } + + /** + * Close internal pool. + * + * @throws ScholixException the scholix exception + */ + protected void closeInternalPool() throws ScholixException { + try { + internalPool.close(); + } catch (Exception e) { + throw new ScholixException("Could not destroy the pool", e); + } + } + + /** + * Gets resource. + * + * @return the resource + * @throws ScholixException the scholix exception + */ + public T getResource() throws ScholixException { + try { + return internalPool.borrowObject(); + } catch (Exception e) { + throw new ScholixException("Could not get a resource from the pool", e); + } + } + + + /** + * Return resource. + * + * @param resource the resource + * @throws ScholixException the scholix exception + */ + public void returnResource(final T resource) throws ScholixException { + if (resource != null) { + returnResourceObject(resource); + } + } + + private void returnResourceObject(final T resource) throws ScholixException { + if (resource == null) { + return; + } + try { + internalPool.returnObject(resource); + } catch (Exception e) { + throw new ScholixException("Could not return the resource to the pool", e); + } + } + + /** + * Return broken resource. + * + * @param resource the resource + * @throws ScholixException the scholix exception + */ + public void returnBrokenResource(final T resource) throws ScholixException { + if (resource != null) { + returnBrokenResourceObject(resource); + } + } + + private void returnBrokenResourceObject(T resource) throws ScholixException { + try { + internalPool.invalidateObject(resource); + } catch (Exception e) { + throw new ScholixException("Could not return the resource to the pool", e); + } + } + + /** + * Destroy. + * + * @throws ScholixException the scholix exception + */ + public void destroy() throws ScholixException { + closeInternalPool(); + } + + + /** + * Gets num active. + * + * @return the num active + */ + public int getNumActive() { + if (poolInactive()) { + return -1; + } + + return this.internalPool.getNumActive(); + } + + /** + * Gets num idle. + * + * @return the num idle + */ + public int getNumIdle() { + if (poolInactive()) { + return -1; + } + + return this.internalPool.getNumIdle(); + } + + /** + * Gets num waiters. + * + * @return the num waiters + */ + public int getNumWaiters() { + if (poolInactive()) { + return -1; + } + + return this.internalPool.getNumWaiters(); + } + + /** + * Gets mean borrow wait time millis. + * + * @return the mean borrow wait time millis + */ + public long getMeanBorrowWaitTimeMillis() { + if (poolInactive()) { + return -1; + } + + return this.internalPool.getMeanBorrowWaitTimeMillis(); + } + + /** + * Gets max borrow wait time millis. + * + * @return the max borrow wait time millis + */ + public long getMaxBorrowWaitTimeMillis() { + if (poolInactive()) { + return -1; + } + + return this.internalPool.getMaxBorrowWaitTimeMillis(); + } + + private boolean poolInactive() { + return this.internalPool == null || this.internalPool.isClosed(); + } + + /** + * Add objects. + * + * @param count the count + * @throws Exception the exception + */ + public void addObjects(int count) throws Exception { + try { + for (int i = 0; i < count; i++) { + this.internalPool.addObject(); + } + } catch (Exception e) { + throw new Exception("Error trying to add idle objects", e); + } + } +} \ No newline at end of file diff --git a/src/main/java/eu/dnetlib/scholexplorer/api/index/ScholixIndexManager.java b/src/main/java/eu/dnetlib/scholexplorer/api/index/ScholixIndexManager.java new file mode 100644 index 0000000..2882444 --- /dev/null +++ b/src/main/java/eu/dnetlib/scholexplorer/api/index/ScholixIndexManager.java @@ -0,0 +1,401 @@ +package eu.dnetlib.scholexplorer.api.index; + + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import eu.dnetlib.dhp.schema.sx.scholix.Scholix; +import eu.dnetlib.dhp.schema.sx.scholix.ScholixEntityId; +import eu.dnetlib.dhp.schema.sx.scholix.ScholixRelationship; +import eu.dnetlib.dhp.schema.sx.scholix.ScholixResource; +import eu.dnetlib.dhp.schema.sx.scholix.flat.ScholixFlat; +import eu.dnetlib.scholexplorer.api.ScholixException; +import eu.dnetlib.scholexplorer.api.TaggedCounter; +import eu.dnetlib.scholexplorer.api.model.Summary; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.tuple.ImmutablePair; +import org.apache.commons.lang3.tuple.Pair; +import org.apache.lucene.search.join.ScoreMode; +import org.elasticsearch.action.search.SearchType; +import org.elasticsearch.client.RestHighLevelClient; +import org.elasticsearch.search.aggregations.AggregationBuilders; +import org.elasticsearch.search.aggregations.Aggregations; +import org.elasticsearch.search.aggregations.bucket.terms.ParsedStringTerms; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.elasticsearch.core.ElasticsearchRestTemplate; +import org.springframework.data.elasticsearch.core.SearchHit; +import org.springframework.data.elasticsearch.core.SearchHits; +import org.springframework.data.elasticsearch.core.mapping.IndexCoordinates; +import org.springframework.stereotype.Component; +import org.springframework.data.elasticsearch.core.query.NativeSearchQuery; +import org.springframework.data.elasticsearch.core.query.NativeSearchQueryBuilder; +import org.elasticsearch.index.query.*; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +@Component +public class ScholixIndexManager { + @Autowired + ElasticSearchProperties elasticSearchProperties; + + /** + * The Elasticsearch template. + */ + @Autowired + ElasticSearchPool connectionPool; + + final ObjectMapper mapper = new ObjectMapper(); + + /** + * The enum Pid type prefix. + */ + public enum RelationPrefix { + /** + * Source pid type prefix. + */ + source, + /** + * Target pid type prefix. + */ + target + } + + @Autowired + TaggedCounter myCounter; + + + private List extractIdentifiersFromScholix(SearchHits scholix) { + return scholix.stream() + .flatMap(s -> + Stream.of( + s.getContent().getSourceId(), + s.getContent().getTargetId())) + .distinct() + .toList(); + } + + + private Map retrieveResources(ElasticsearchRestTemplate client, List ids) { + final IdsQueryBuilder qb = new IdsQueryBuilder().addIds(ids.toArray(String[]::new)); + + + final NativeSearchQuery searchQuery = new NativeSearchQueryBuilder() + .withQuery(qb) + .withPageable(PageRequest.of(0, ids.size())) + .build(); + + + SearchHits result = client.search(searchQuery, Summary.class, IndexCoordinates.of(elasticSearchProperties.getIndexResourceName())); + return result.stream().map(r -> { + try { + return mapper.readValue(r.getContent().getBody(), ScholixResource.class); + } catch (JsonProcessingException e) { + throw new RuntimeException(e); + } + }).collect(Collectors.toMap( + ScholixResource::getDnetIdentifier, + v -> v, + (a, b) -> a + )); + + } + + private Scholix generateScholix(ScholixFlat flat, ScholixResource source, ScholixResource target) throws ScholixException { + if (flat == null || source == null || target == null) + throw new ScholixException("Error generating scholix null input"); + + final Scholix scholix = new Scholix(); + scholix.setSource(source); + scholix.setTarget(target); + scholix.setIdentifier(flat.getIdentifier()); + final ScholixRelationship r = new ScholixRelationship(); + r.setSchema("datacite"); + r.setName(flat.getRelationType().toLowerCase()); + scholix.setRelationship(r); + scholix.setPublicationDate(flat.getPublicationDate()); + scholix.setLinkprovider(flat.getLinkProviders().stream().map(p -> { + final ScholixEntityId eid = new ScholixEntityId(); + eid.setName(p); + return eid; + }).toList()); + + final Map publishers = new HashMap<>(); + if (source.getPublisher() != null) + source.getPublisher().forEach(p -> publishers.put(p.getName(), p)); + if (target.getPublisher() != null) + target.getPublisher().forEach(p -> publishers.put(p.getName(), p)); + + scholix.setPublisher(publishers.values().stream().toList()); + return scholix; + } + + private QueryBuilder createFinalQuery(final List queries) throws ScholixException { + + if (queries == null || queries.isEmpty()) + throw new ScholixException("the list of queries must be not empty"); + + + if (queries.size() == 1) { + return queries.get(0); + } else { + final BoolQueryBuilder b = new BoolQueryBuilder(); + b.must().addAll(queries); + + return b; + } + + } + + public List> totalLinksByProvider(final String filterName) throws ScholixException { + + + final QueryBuilder query = StringUtils.isNoneBlank(filterName) ? QueryBuilders.termQuery("linkProviders", filterName) : QueryBuilders.matchAllQuery(); + + final NativeSearchQuery searchQuery = new NativeSearchQueryBuilder() + .withQuery(query) + .withSearchType(SearchType.DEFAULT) + .withPageable(PageRequest.of(0, 10)) + .addAggregation( + AggregationBuilders.terms("genres").field("linkProviders").size(100) + .minDocCount(1)) + .build(); + + + Pair resource = null; + try { + resource = connectionPool.getResource(); + ElasticsearchRestTemplate client = resource.getValue(); + final SearchHits hits = client.search(searchQuery, ScholixFlat.class, IndexCoordinates.of(elasticSearchProperties.getIndexName())); + + final Aggregations aggregations = hits.getAggregations(); + if (aggregations == null) + return null; + + return ((ParsedStringTerms) aggregations.get("genres")).getBuckets().stream().map(b -> new ImmutablePair<>(b.getKeyAsString(), b.getDocCount())).collect(Collectors.toList()); + } catch (ScholixException e) { + throw e; + } finally { + if (connectionPool != null) { + connectionPool.returnResource(resource); + } + } + + + } + + + private QueryBuilder createLinkPublisherQuery(final RelationPrefix prefix, final String publisher) throws ScholixException { + if (prefix == null) { + throw new ScholixException("prefix cannot be null"); + } + return new NestedQueryBuilder(String.format("%s.publisher", prefix), new TermQueryBuilder(String.format("%s.publisher.name", prefix), publisher), ScoreMode.None); + } + + + public List> totalLinksPublisher(final RelationPrefix prefix, final String filterName) throws ScholixException { + + + final QueryBuilder query = StringUtils.isNoneBlank(filterName) ? createLinkPublisherQuery(prefix, filterName) : QueryBuilders.matchAllQuery(); + + final NativeSearchQuery searchQuery = new NativeSearchQueryBuilder() + .withQuery(query) + .withSearchType(SearchType.DEFAULT) + .withPageable(PageRequest.of(0, 10)) + .addAggregation( + AggregationBuilders.terms("publishers").field(String.format("%sPublisher", prefix.toString())).size(100) + .minDocCount(1)) + .build(); + + + Pair resource = null; + try { + resource = connectionPool.getResource(); + ElasticsearchRestTemplate client = resource.getValue(); + final SearchHits hits = client.search(searchQuery, ScholixFlat.class, IndexCoordinates.of(elasticSearchProperties.getIndexName())); + + final Aggregations aggregations = hits.getAggregations(); + if (aggregations == null) + return null; + + return ((ParsedStringTerms) aggregations.get("publishers")).getBuckets().stream().map(b -> new ImmutablePair<>(b.getKeyAsString(), b.getDocCount())).collect(Collectors.toList()); + } catch (ScholixException e) { + throw e; + } finally { + if (connectionPool != null) { + connectionPool.returnResource(resource); + } + } + } + + private void incrementPidCounter(RelationPrefix prefix, String value) { + switch (value.toLowerCase()) { + case "doi": { + myCounter.increment(String.format("%s_doi", prefix)); + break; + } + case "pmc": { + myCounter.increment(String.format("%s_pmc", prefix)); + break; + } + default: + myCounter.increment(String.format("%s_other", prefix)); + } + } + + + /** + * Links from pid pair. + * + * @param linkProvider the link provider + * @param targetPid the target pid + * @param targetPidType the target pid type + * @param targetPublisher the target publisher + * @param targetType the target type + * @param sourcePid the source pid + * @param sourcePidType the source pid type + * @param sourcePublisher the source publisher + * @param sourceType the source type + * @param page the page + * @return the pair + * @throws ScholixException the scholix exception + */ + public Pair> linksFromPid(final String linkProvider, + final String targetPid, final String targetPidType, final String targetPublisher, + final String targetType, final String sourcePid, final String sourcePidType, + final String sourcePublisher, + final String sourceType, + final String relation, + final Integer page) throws ScholixException { + + + if (sourcePid == null && sourcePidType == null && sourceType == null && targetType == null && targetPid == null && targetPidType == null && sourcePublisher == null && targetPublisher == null && linkProvider == null) + throw new ScholixException("One of sourcePid, targetPid, sourcePublisher, targetPublisher, linkProvider should be not null"); + + final List queries = new ArrayList<>(); + + if (StringUtils.isNoneBlank(linkProvider)) { + myCounter.increment("linkProvider"); + queries.add(QueryBuilders.termQuery("linkProviders", linkProvider)); + } + + if (StringUtils.isNoneBlank(targetPid)) { + myCounter.increment("targetPid"); + queries.add(QueryBuilders.termQuery("targetPid", targetPid)); + } + if (StringUtils.isNoneBlank(sourcePid)) { + myCounter.increment("sourcePid"); + queries.add(QueryBuilders.termQuery("sourcePid", sourcePid)); + } + + if (StringUtils.isNoneBlank(targetPidType)) { + assert targetPidType != null; + incrementPidCounter(RelationPrefix.target, targetPidType); + queries.add(QueryBuilders.termQuery("targetPidType", targetPidType)); + } + if (StringUtils.isNoneBlank(sourcePidType)) { + assert sourcePidType != null; + incrementPidCounter(RelationPrefix.source, sourcePidType); + queries.add(QueryBuilders.termQuery("sourcePidType", sourcePidType)); + } + + if (StringUtils.isNoneBlank(targetType)) { + myCounter.increment(String.format("targetType_%s", targetType)); + queries.add(QueryBuilders.termQuery("targetType", targetType)); + } + + if (StringUtils.isNoneBlank(sourceType)) { + assert sourceType != null; + myCounter.increment(String.format("sourceType_%s", sourceType)); + queries.add(QueryBuilders.termQuery("sourceType", sourceType)); + } + + if (StringUtils.isNoneBlank(targetPublisher)) { + myCounter.increment("targetPublisher"); + queries.add(QueryBuilders.termQuery("targetPublisher", targetPublisher)); + } + + if (StringUtils.isNoneBlank(relation)) { + myCounter.increment("targetPublisher"); + queries.add(QueryBuilders.termQuery("relationType", relation)); + } + QueryBuilder result = createFinalQuery(queries); + + NativeSearchQuery finalQuery = new NativeSearchQueryBuilder() + .withQuery(result) + .withPageable(PageRequest.of(page, 100)) + .build(); + + + Pair resource = null; + try { + resource = connectionPool.getResource(); + ElasticsearchRestTemplate client = resource.getValue(); + + + long tt = client.count(finalQuery, ScholixFlat.class, IndexCoordinates.of(elasticSearchProperties.getIndexName())); + + SearchHits scholixRes = client.search(finalQuery, ScholixFlat.class, IndexCoordinates.of(elasticSearchProperties.getIndexName())); + + + if (tt > 0) { + final Map idMap = retrieveResources(client, extractIdentifiersFromScholix(scholixRes)); + + return new ImmutablePair<>(tt, scholixRes.stream().map(SearchHit::getContent).map(s -> { + try { + return generateScholix(s, idMap.get(s.getSourceId()), idMap.get(s.getTargetId())); + } catch (ScholixException e) { + throw new RuntimeException(e); + } + }).toList()); + } else return new ImmutablePair<>(tt, new ArrayList<>()); + } catch (ScholixException e) { + throw e; + } finally { + if (connectionPool != null) { + connectionPool.returnResource(resource); + } + + } + } + + public List findPage(final String relType) throws ScholixException { + Pair resource = null; + try { + resource = connectionPool.getResource(); + ElasticsearchRestTemplate client = resource.getValue(); + final NativeSearchQuery searchQuery = new NativeSearchQueryBuilder() + + .withQuery(QueryBuilders.termQuery("relationType", relType)) + + .withSearchType(SearchType.DEFAULT) + .withPageable(PageRequest.of(0, 20)) + .build(); + + SearchHits search = client.search(searchQuery, ScholixFlat.class, IndexCoordinates.of(elasticSearchProperties.getIndexName())); + + + final Map idMap = retrieveResources(client, extractIdentifiersFromScholix(search)); + + return search.stream().map(SearchHit::getContent).map(s -> { + try { + return generateScholix(s, idMap.get(s.getSourceId()), idMap.get(s.getTargetId())); + } catch (ScholixException e) { + throw new RuntimeException(e); + } + }).toList(); + } catch (Throwable e) { + System.out.println(e.getMessage()); + } finally { + if (connectionPool != null && resource != null) { + connectionPool.returnResource(resource); + } + } + return null; + } + +} diff --git a/src/main/java/eu/dnetlib/scholexplorer/api/model/Summary.java b/src/main/java/eu/dnetlib/scholexplorer/api/model/Summary.java new file mode 100644 index 0000000..5472f83 --- /dev/null +++ b/src/main/java/eu/dnetlib/scholexplorer/api/model/Summary.java @@ -0,0 +1,13 @@ +package eu.dnetlib.scholexplorer.api.model; + +public class Summary { + private String body; + + public String getBody() { + return body; + } + + public void setBody(String body) { + this.body = body; + } +} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties new file mode 100644 index 0000000..9b7468a --- /dev/null +++ b/src/main/resources/application.properties @@ -0,0 +1,43 @@ +spring.application.name=scholexplorer-api +spring.main.banner-mode = console +springdoc.api-docs.path=/api-docs +springdoc.swagger-ui.path=/scholexplorer-api +server.public_url = +server.public_desc = API Base URL + +logging.level.root = INFO +dhp.swagger.api.host = localhost:8080 +#dhp.swagger.api.host = api.scholexplorer.openaire.eu +dhp.swagger.api.basePath = / + + +management.endpoints.web.exposure.include = health, metrics, prometheus +management.metrics.tags.application=${spring.application.name} +#maven.pom.path = /META-INF/maven/eu.dnetlib.dhp/scholexplorer-api/effective-pom.xml + +# +#spring.thymeleaf.cache=false +# +#spring.metrics.export.prometheus.enabled = true +#spring.metrics.export.prometheus.port = 8080 +#management.endpoints.web.exposure.include = phealth, metrics, prometheus +#management.endpoints.web.base-path = / +#management.endpoints.web.path-mapping.prometheus = metrics +#management.endpoints.web.path-mapping.health = health +#management.endpoint.health.show-details = always +# +#management.metrics.distribution.percentiles-histogram.http.server.requests=false +#management.metrics.distribution.slo.http.server.requests=50ms, 100ms, 200ms, 400ms +#management.metrics.distribution.percentiles.http.server.requests=0.5, 0.9, 0.95, 0.99, 0.999 + + + +#scholix.elastic.clusterNodes = 10.19.65.51:9200,10.19.65.52:9200,10.19.65.53:9200,10.19.65.54:9200 +scholix.elastic.clusterNodes = localhost:9200 +scholix.elastic.indexName = scholix +scholix.elastic.indexResourceName = summary +scholix.elastic.socketTimeout = 60000 +scholix.elastic.connectionTimeout= 60000 + + + diff --git a/src/main/resources/static/logo.png b/src/main/resources/static/logo.png new file mode 100644 index 0000000..5291183 Binary files /dev/null and b/src/main/resources/static/logo.png differ diff --git a/src/main/resources/static/logo.svg b/src/main/resources/static/logo.svg new file mode 100644 index 0000000..08ab481 --- /dev/null +++ b/src/main/resources/static/logo.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/test/java/eu/dnetlib/scholexplorer/api/ScholexplorerApiApplicationTests.java b/src/test/java/eu/dnetlib/scholexplorer/api/ScholexplorerApiApplicationTests.java new file mode 100644 index 0000000..e1cfa3f --- /dev/null +++ b/src/test/java/eu/dnetlib/scholexplorer/api/ScholexplorerApiApplicationTests.java @@ -0,0 +1,173 @@ +package eu.dnetlib.scholexplorer.api; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import eu.dnetlib.dhp.schema.sx.scholix.Scholix; +import eu.dnetlib.scholexplorer.api.index.ScholixIndexManager; +import org.apache.commons.lang3.tuple.Pair; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import java.util.*; + + +class TestResult { + public long totalTimeMs; + public int totalItems; + public String relationName; + + public long getTotalTimeMs() { + return totalTimeMs; + } + + public TestResult setTotalTimeMs(long totalTimeMs) { + this.totalTimeMs = totalTimeMs; + return this; + } + + public int getTotalItems() { + return totalItems; + } + + public TestResult setTotalItems(int totalItems) { + this.totalItems = totalItems; + return this; + } + + public String getRelationName() { + return relationName; + } + + public TestResult setRelationName(String relationName) { + this.relationName = relationName; + return this; + } +} + +@SpringBootTest +class ScholexplorerApiApplicationTests { + + @Autowired + ScholixIndexManager scholixRepo; + + final ObjectMapper mapper = new ObjectMapper(); + + + final List relations = Arrays.asList("IsDerivedFrom", + "Continues", + "References", + "HasVersion", + "IsVersionOf", + "IsSupplementTo", + "IsDocumentedBy", + "Documents", + "IsContinuedBy", + "IsSupplementedBy", + "IsSourceOf", + "Reviews", + "Cites", + "IsAmongTopNSimilarDocuments", + "IsPartOf", + "HasPart", + "IsIdenticalTo", + "IsNewVersionOf", + "IsRelatedTo", + "HasAmongTopNSimilarDocuments", + "IsCitedBy", + "IsPreviousVersionOf", + "IsReferencedBy", + "IsVariantFormOf", + "IsCompiledBy", + "IsReviewedBy", + "IsDescribedBy", + "Compiles", + "IsOriginalFormOf", + "Describes"); + + private List executeTest() throws Exception { + Random rand = new Random(); + + final List infos = new ArrayList<>(); + String currentRel1 =relations.get(rand.nextInt(relations.size())); + long start = System.nanoTime(); + List result = scholixRepo.findPage(currentRel1); + long total = (System.nanoTime() - start) /1000000; + int cnt = result.size(); + + infos.add(new TestResult().setRelationName(currentRel1).setTotalItems(cnt).setTotalTimeMs(total)); + + String currentRel2 =relations.get(rand.nextInt(relations.size())); + start = System.nanoTime(); + result = scholixRepo.findPage(currentRel2); + long total2 = (System.nanoTime() - start) /1000000; + int cnt2 = result.size(); + infos.add(new TestResult().setRelationName(currentRel2).setTotalItems(cnt2).setTotalTimeMs(total2)); + + String currentRel3 =relations.get(rand.nextInt(relations.size())); + start = System.nanoTime(); + result = scholixRepo.findPage(currentRel3); + long total3 = (System.nanoTime() - start) /1000000; + int cnt3 = result.size(); + infos.add(new TestResult().setRelationName(currentRel3).setTotalItems(cnt3).setTotalTimeMs(total3)); + return infos; + } + + @Test + void contextLoads() throws Exception { + final List infos = new ArrayList<>(); + for (int i = 0; i < 200; i++) { + infos.addAll(executeTest()); + } + + + LongSummaryStatistics summary = infos.stream().map(TestResult::getTotalTimeMs).mapToLong(Long::longValue).summaryStatistics(); + + System.out.println(summary.getMax()); + System.out.println(summary.getMin()); + System.out.println(summary.getAverage()); + } + + + + @Test + void testlinksFromPid() throws ScholixException { + Pair> result = scholixRepo.linksFromPid(null, null, "doi", null, null, null, "pmid", null, null, null, 0); + result.getRight().forEach( + s -> Assertions.assertTrue(s.getTarget().getIdentifier().stream().anyMatch(p -> p.getSchema().equals("doi"))) + ); + + result.getRight().forEach( + s -> Assertions.assertTrue(s.getSource().getIdentifier().stream().anyMatch(p -> p.getSchema().equals("pmid"))) + ); + + + result = scholixRepo.linksFromPid(null, null, null, null, "dataset", null, null, null, "publication","IsSupplementedBy", 0); + System.out.println(result.getLeft()); + result.getRight().forEach( + s -> { + Assertions.assertEquals("dataset", s.getTarget().getObjectType()); + Assertions.assertEquals("publication", s.getSource().getObjectType()); + Assertions.assertEquals("issupplementedby", s.getRelationship().getName()); + + }); + + result = scholixRepo.linksFromPid(null, null, null, null, "publication", null, null, null, "publication","IsVersionOf", 0); + System.out.println(result.getLeft()); + result.getRight().forEach( + s -> { + Assertions.assertEquals("publication", s.getTarget().getObjectType()); + Assertions.assertEquals("publication", s.getSource().getObjectType()); + Assertions.assertEquals("IsVersionOf".toLowerCase(), s.getRelationship().getName()); + + }); + + + + } + + + + +} diff --git a/src/test/resources/application.properties b/src/test/resources/application.properties new file mode 100644 index 0000000..5ee330f --- /dev/null +++ b/src/test/resources/application.properties @@ -0,0 +1,38 @@ +spring.application.name=scholexplorer-api +spring.main.banner-mode = console + +server.public_url = +server.public_desc = API Base URL + +logging.level.root = INFO +dhp.swagger.api.host = localhost:8080 +#dhp.swagger.api.host = api.scholexplorer.openaire.eu +dhp.swagger.api.basePath = / + +maven.pom.path = /META-INF/maven/eu.dnetlib.dhp/scholexplorer-api/effective-pom.xml + +# +#spring.thymeleaf.cache=false +# +management.endpoints.web.exposure.include = prometheus,health +management.endpoints.web.base-path = / +management.endpoints.web.path-mapping.prometheus = metrics +management.endpoints.web.path-mapping.health = health +management.endpoint.health.show-details = always + +management.metrics.distribution.percentiles-histogram.http.server.requests=false +management.metrics.distribution.slo.http.server.requests=50ms, 100ms, 200ms, 400ms +management.metrics.distribution.percentiles.http.server.requests=0.5, 0.9, 0.95, 0.99, 0.999 + + + +#scholix.elastic.clusterNodes = 10.19.65.51:9200,10.19.65.52:9200,10.19.65.53:9200,10.19.65.54:9200 +scholix.elastic.clusterNodes = localhost:9200 +scholix.elastic.indexName = scholix +scholix.elastic.indexResourceName = summary +scholix.elastic.socketTimeout = 60000 +scholix.elastic.connectionTimeout= 60000 + + + +