package eu.dnetlib.broker.openaire; import java.util.ArrayList; import java.util.Arrays; import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.stream.Collectors; import java.util.stream.StreamSupport; import org.apache.commons.io.IOUtils; import org.apache.commons.lang3.StringUtils; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.apache.lucene.search.join.ScoreMode; import org.elasticsearch.action.search.SearchType; import org.elasticsearch.index.query.BoolQueryBuilder; import org.elasticsearch.index.query.NestedQueryBuilder; import org.elasticsearch.index.query.Operator; import org.elasticsearch.index.query.QueryBuilder; import org.elasticsearch.index.query.QueryBuilders; import org.elasticsearch.search.aggregations.Aggregation; import org.elasticsearch.search.aggregations.AggregationBuilders; import org.elasticsearch.search.aggregations.Aggregations; import org.elasticsearch.search.aggregations.bucket.nested.ParsedNested; import org.elasticsearch.search.aggregations.bucket.terms.ParsedStringTerms; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Profile; import org.springframework.data.domain.PageRequest; import org.springframework.data.elasticsearch.core.ElasticsearchOperations; 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.data.elasticsearch.core.query.NativeSearchQuery; import org.springframework.data.elasticsearch.core.query.NativeSearchQueryBuilder; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.core.RowMapper; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import eu.dnetlib.broker.LiteratureBrokerServiceApplication; import eu.dnetlib.broker.common.elasticsearch.Event; import eu.dnetlib.broker.common.elasticsearch.Notification; import eu.dnetlib.broker.common.elasticsearch.NotificationRepository; import eu.dnetlib.broker.common.properties.ElasticSearchProperties; import eu.dnetlib.broker.common.subscriptions.MapCondition; import eu.dnetlib.broker.common.subscriptions.Subscription; import eu.dnetlib.broker.common.subscriptions.SubscriptionRepository; import eu.dnetlib.broker.events.output.DispatcherManager; import eu.dnetlib.broker.objects.OaBrokerEventPayload; import eu.dnetlib.common.controller.AbstractDnetController; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; @Profile("openaire") @RestController @RequestMapping("/api/openaireBroker") @Tag(name = LiteratureBrokerServiceApplication.TAG_OPENAIRE) public class OpenaireBrokerController extends AbstractDnetController { @Autowired private ElasticsearchOperations esOperations; @Autowired private NotificationRepository notificationRepository; @Autowired private SubscriptionRepository subscriptionRepo; @Autowired private ElasticSearchProperties props; @Autowired private DispatcherManager dispatcher; @Autowired private JdbcTemplate jdbcTemplate; private static final Log log = LogFactory.getLog(OpenaireBrokerController.class); @Operation(summary = "Return the datasources having events") @GetMapping("/datasources") public List findDatasourcesWithEvents(@RequestParam(defaultValue = "false", required = false) final boolean useIndex) { return useIndex ? findDatasourcesWithEventsUsingIndex() : findDatasourcesWithEventsUsingDb(); } private List findDatasourcesWithEventsUsingIndex() { final NativeSearchQuery searchQuery = new NativeSearchQueryBuilder() .withQuery(QueryBuilders.matchAllQuery()) .withSearchType(SearchType.DEFAULT) .addAggregation(AggregationBuilders.nested("nested", "map") // .path("map") .subAggregation(AggregationBuilders.terms("by_map").field("map.targetDatasourceName").size(1000).minDocCount(1))) .build(); final SearchHits hits = esOperations.search(searchQuery, Event.class, IndexCoordinates.of(props.getEventsIndexName())); final Aggregations aggregations = hits.getAggregations(); final Aggregation aggByMap = ((ParsedNested) aggregations.asMap().get("nested")).getAggregations().asMap().get("by_map"); return ((ParsedStringTerms) aggByMap).getBuckets() .stream() .map(b -> new BrowseEntry(b.getKeyAsString(), b.getDocCount())) .collect(Collectors.toList()); } private List findDatasourcesWithEventsUsingDb() { try { final String sql = IOUtils.toString(getClass().getResourceAsStream("/sql/datasourceTopics.sql")); final RowMapper mapper = (rs, rowNum) -> new BrowseEntry(rs.getString("name"), rs.getLong("size")); return jdbcTemplate.query(sql, mapper); } catch (final Exception e) { log.error("Error executing query", e); return new ArrayList<>(); } } @Operation(summary = "Return the topics of the events of a datasource") @GetMapping("/topicsForDatasource") public List findTopicsForDatasource(@RequestParam final String ds, @RequestParam(defaultValue = "false", required = false) final boolean useIndex) { return useIndex ? findTopicsForDatasourceUsingIndex(ds) : findTopicsForDatasourceUsingDb(ds); } private List findTopicsForDatasourceUsingIndex(final String ds) { final String term = "topic.keyword"; final NativeSearchQuery searchQuery = new NativeSearchQueryBuilder() .withQuery(QueryBuilders.nestedQuery("map", QueryBuilders.matchQuery("map.targetDatasourceName", ds), ScoreMode.None)) .withSearchType(SearchType.DEFAULT) .addAggregation(AggregationBuilders.terms(term).field(term).size(1000).minDocCount(1)) .build(); final SearchHits hits = esOperations.search(searchQuery, Event.class, IndexCoordinates.of(props.getEventsIndexName())); final Aggregations aggregations = hits.getAggregations(); return ((ParsedStringTerms) aggregations.asMap().get(term)).getBuckets() .stream() .map(b -> new BrowseEntry(b.getKeyAsString(), b.getDocCount())) .collect(Collectors.toList()); } private List findTopicsForDatasourceUsingDb(final String ds) { try { final String sql = IOUtils.toString(getClass().getResourceAsStream("/sql/datasourceTopicsDetailed.sql")); final RowMapper mapper = (rs, rowNum) -> new BrowseEntry(rs.getString("topic"), rs.getLong("size")); return jdbcTemplate.query(sql, new Object[] { ds }, mapper); } catch (final Exception e) { log.error("Error executing query", e); return new ArrayList<>(); } } @Operation(summary = "Return a page of events of a datasource (by topic)") @GetMapping("/events/{nPage}/{size}") public EventsPage showEvents(@RequestParam final String ds, @RequestParam final String topic, @PathVariable final int nPage, @PathVariable final int size) { final NestedQueryBuilder nestedQuery = QueryBuilders.nestedQuery("map", QueryBuilders.matchQuery("map.targetDatasourceName", ds), ScoreMode.None); final QueryBuilder q = StringUtils.isNotBlank(topic) && !topic.equals("*") ? QueryBuilders.boolQuery() .must(QueryBuilders.matchQuery("topic", topic).operator(Operator.AND)) .must(nestedQuery) : nestedQuery; final NativeSearchQuery searchQuery = new NativeSearchQueryBuilder() .withQuery(q) .withSearchType(SearchType.DEFAULT) .withFields("payload") .withPageable(PageRequest.of(nPage, size)) .build(); final SearchHits page = esOperations.search(searchQuery, Event.class, IndexCoordinates.of(props.getEventsIndexName())); final List list = page.stream() .map(SearchHit::getContent) .map(Event::getPayload) .map(OaBrokerEventPayload::fromJSON) .collect(Collectors.toList()); return new EventsPage(ds, topic, nPage, overrideGetTotalPage(page, size), page.getTotalHits(), list); } @Operation(summary = "Return a page of events of a datasource (by query)") @PostMapping("/events/{nPage}/{size}") public EventsPage advancedShowEvents(@PathVariable final int nPage, @PathVariable final int size, @RequestBody final AdvQueryObject qObj) { final BoolQueryBuilder mapQuery = QueryBuilders.boolQuery(); ElasticSearchQueryUtils.addMapCondition(mapQuery, "map.targetDatasourceName", qObj.getDatasource()); ElasticSearchQueryUtils.addMapCondition(mapQuery, "map.targetResultTitle", qObj.getTitles()); ElasticSearchQueryUtils.addMapCondition(mapQuery, "map.targetAuthors", qObj.getAuthors()); ElasticSearchQueryUtils.addMapCondition(mapQuery, "map.targetSubjects", qObj.getSubjects()); ElasticSearchQueryUtils.addMapConditionForTrust(mapQuery, "map.trust", qObj.getTrust()); ElasticSearchQueryUtils.addMapConditionForDates(mapQuery, "map.targetDateofacceptance", qObj.getDates()); final NestedQueryBuilder nestedQuery = QueryBuilders.nestedQuery("map", mapQuery, ScoreMode.None); final QueryBuilder q = StringUtils.isNotBlank(qObj.getTopic()) && !qObj.getTopic().equals("*") ? QueryBuilders.boolQuery() .must(QueryBuilders.matchQuery("topic", qObj.getTopic()).operator(Operator.AND)) .must(nestedQuery) : nestedQuery; final NativeSearchQuery searchQuery = new NativeSearchQueryBuilder() .withQuery(q) .withSearchType(SearchType.DEFAULT) .withFields("payload") .withPageable(PageRequest.of(nPage, size)) .build(); final SearchHits page = esOperations.search(searchQuery, Event.class, IndexCoordinates.of(props.getEventsIndexName())); final List list = page.stream() .map(SearchHit::getContent) .map(Event::getPayload) .map(OaBrokerEventPayload::fromJSON) .collect(Collectors.toList()); return new EventsPage(qObj.getDatasource(), qObj.getTopic(), nPage, overrideGetTotalPage(page, size), page.getTotalHits(), list); } @Operation(summary = "Perform a subscription") @PostMapping("/subscribe") public Subscription registerSubscription(@RequestBody final OpenaireSubscription oSub) { final Subscription sub = oSub.asSubscription(); subscriptionRepo.save(sub); return sub; } @Operation(summary = "Return the subscriptions of an user (by email and datasource (optional))") @GetMapping("/subscriptions") public Map> subscriptions(@RequestParam final String email, @RequestParam(required = false) final String ds) { final Iterable iter = subscriptionRepo.findBySubscriber(email); return StreamSupport.stream(iter.spliterator(), false) .map(this::subscriptionDesc) .filter(s -> StringUtils.isBlank(ds) || StringUtils.equalsIgnoreCase(s.getDatasource(), ds)) .collect(Collectors.groupingBy(SimpleSubscriptionDesc::getDatasource)); } @Operation(summary = "Return a page of notifications") @GetMapping("/notifications/{subscrId}/{nPage}/{size}") public EventsPage notifications(@PathVariable final String subscrId, @PathVariable final int nPage, @PathVariable final int size) { final Optional optSub = subscriptionRepo.findById(subscrId); if (optSub.isPresent()) { final Subscription sub = optSub.get(); final NativeSearchQuery searchQuery = new NativeSearchQueryBuilder() .withQuery(QueryBuilders.termQuery("subscriptionId.keyword", subscrId)) .withSearchType(SearchType.DEFAULT) .withFields("payload") .withPageable(PageRequest.of(nPage, size)) .build(); final SearchHits page = esOperations.search(searchQuery, Notification.class, IndexCoordinates.of(props.getNotificationsIndexName())); final List list = page.stream() .map(SearchHit::getContent) .map(Notification::getPayload) .map(OaBrokerEventPayload::fromJSON) .collect(Collectors.toList()); return new EventsPage(extractDatasource(sub), sub.getTopic(), nPage, overrideGetTotalPage(page, size), page.getTotalHits(), list); } else { log.warn("Invalid subscription: " + subscrId); return new EventsPage("", "", nPage, 0, 0, new ArrayList<>()); } } @Operation(summary = "Send notifications") @GetMapping("/notifications/send/{date}") private List sendMailForNotifications(@PathVariable final long date) { new Thread(() -> innerSendMailForNotifications(date)).start(); return Arrays.asList("Sending ..."); } @Operation(summary = "Update stats") @GetMapping("/stats/update") private List updateStats() { new Thread(() -> { try { jdbcTemplate.update(IOUtils.toString(getClass().getResourceAsStream("/sql/updateStats.sql"))); } catch (final Exception e) { log.error("Error updating stats", e); } }).start(); return Arrays.asList("Sending ..."); } private void innerSendMailForNotifications(final long date) { for (final Subscription s : subscriptionRepo.findAll()) { final long count = notificationRepository.countBySubscriptionIdAndDateAfter(s.getSubscriptionId(), date); if (count > 0) { final Map params = new HashMap<>(); params.put("oa_notifications_total", count); params.put("oa_datasource", extractDatasource(s)); dispatcher.sendNotification(s, params); } s.setLastNotificationDate(new Date()); subscriptionRepo.save(s); } } private SimpleSubscriptionDesc subscriptionDesc(final Subscription s) { return new SimpleSubscriptionDesc(s.getSubscriptionId(), extractDatasource(s), s.getTopic(), s.getCreationDate(), s.getLastNotificationDate(), OpenaireBrokerController.this.notificationRepository.countBySubscriptionId(s.getSubscriptionId())); } private String extractDatasource(final Subscription sub) { return sub.getConditionsAsList() .stream() .filter(c -> c.getField().equals("targetDatasourceName")) .map(MapCondition::getListParams) .filter(l -> !l.isEmpty()) .map(l -> l.get(0).getValue()) .findFirst() .orElse(""); } private long overrideGetTotalPage(final SearchHits page, final int size) { return (page.getTotalHits() + size - 1) / size; } }