Skip to content

Commit

Permalink
[backend] Improv performance on articles list (#898)
Browse files Browse the repository at this point in the history
  • Loading branch information
RomuDeuxfois authored Jun 11, 2024
1 parent a41c76c commit 379d4ac
Show file tree
Hide file tree
Showing 8 changed files with 293 additions and 56 deletions.
60 changes: 7 additions & 53 deletions openbas-api/src/main/java/io/openbas/rest/channel/ChannelApi.java
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,11 @@
import java.util.stream.Collectors;

import static io.openbas.config.OpenBASAnonymous.ANONYMOUS;
import static io.openbas.database.model.Inject.SPEED_STANDARD;
import static io.openbas.database.model.User.ROLE_ADMIN;
import static io.openbas.helper.StreamHelper.fromIterable;
import static io.openbas.injectors.channel.ChannelContract.CHANNEL_PUBLISH;
import static io.openbas.rest.channel.ChannelHelper.enrichArticleWithVirtualPublication;
import static io.openbas.rest.scenario.ScenarioApi.SCENARIO_URI;
import static java.util.Comparator.naturalOrder;
import static java.util.Comparator.nullsFirst;

@RestController
public class ChannelApi extends RestBehavior {
Expand Down Expand Up @@ -131,36 +129,6 @@ public void deleteChannel(@PathVariable String channelId) {
channelRepository.deleteById(channelId);
}

private List<Article> enrichArticleWithVirtualPublication(List<Inject> injects, List<Article> articles) {
Instant now = Instant.now();
Map<String, Instant> toPublishArticleIdsMap = injects.stream()
.filter(inject -> inject.getInjectorContract().getId().equals(CHANNEL_PUBLISH))
.filter(inject -> inject.getContent() != null)
// TODO take into account depends_another here, depends_duration is not enough to order articles
.sorted(Comparator.comparing(Inject::getDependsDuration))
.flatMap(inject -> {
Instant virtualInjectDate = inject.computeInjectDate(now, SPEED_STANDARD);
try {
ChannelContent content = mapper.treeToValue(inject.getContent(), ChannelContent.class);
return content.getArticles().stream().map(article -> new VirtualArticle(virtualInjectDate, article));
} catch (JsonProcessingException e) {
// Invalid channel content.
return null;
}
})
.filter(Objects::nonNull)
.distinct()
.collect(Collectors.toMap(VirtualArticle::id, VirtualArticle::date));
return articles.stream()
.peek(article -> article.setVirtualPublication(toPublishArticleIdsMap.get(article.getId())))
.sorted(Comparator.comparing(Article::getVirtualPublication, nullsFirst(naturalOrder()))
.thenComparing(Article::getCreatedAt).reversed()).toList();
}

private Article enrichArticleWithVirtualPublication(List<Inject> injects, Article article) {
return enrichArticleWithVirtualPublication(injects, List.of(article)).stream().findFirst().orElseThrow(ElementNotFoundException::new);
}

@GetMapping("/api/observer/channels/{exerciseId}/{channelId}")
@PreAuthorize("isExerciseObserver(#exerciseId)")
public ChannelReader observerArticles(@PathVariable String exerciseId, @PathVariable String channelId) {
Expand All @@ -172,13 +140,13 @@ public ChannelReader observerArticles(@PathVariable String exerciseId, @PathVari
Exercise exercise = exerciseOpt.get();
channelReader = new ChannelReader(channel, exercise);
List<Article> publishedArticles = exercise.getArticlesForChannel(channel);
List<Article> articles = enrichArticleWithVirtualPublication(exercise.getInjects(), publishedArticles);
List<Article> articles = enrichArticleWithVirtualPublication(exercise.getInjects(), publishedArticles, this.mapper);
channelReader.setChannelArticles(articles);
} else {
Scenario scenario = this.scenarioService.scenario(exerciseId);
channelReader = new ChannelReader(channel, scenario);
List<Article> publishedArticles = scenario.getArticlesForChannel(channel);
List<Article> articles = enrichArticleWithVirtualPublication(scenario.getInjects(), publishedArticles);
List<Article> articles = enrichArticleWithVirtualPublication(scenario.getInjects(), publishedArticles, this.mapper);
channelReader.setChannelArticles(articles);
}
return channelReader;
Expand Down Expand Up @@ -283,14 +251,7 @@ public Article createArticleForExercise(
}
});
savedArticle.setDocuments(finalArticleDocuments);
return enrichArticleWithVirtualPublication(exercise.getInjects(), savedArticle);
}

@PreAuthorize("isExerciseObserver(#exerciseId)")
@GetMapping("/api/exercises/{exerciseId}/articles")
public Iterable<Article> exerciseArticles(@PathVariable String exerciseId) {
Exercise exercise = exerciseRepository.findById(exerciseId).orElseThrow(ElementNotFoundException::new);
return enrichArticleWithVirtualPublication(exercise.getInjects(), exercise.getArticles());
return enrichArticleWithVirtualPublication(exercise.getInjects(), savedArticle, this.mapper);
}

@PreAuthorize("isExercisePlanner(#exerciseId)")
Expand Down Expand Up @@ -327,7 +288,7 @@ public Article updateArticleForExercise(
});
article.setDocuments(articleDocuments);
Article savedArticle = articleRepository.save(article);
return enrichArticleWithVirtualPublication(exercise.getInjects(), savedArticle);
return enrichArticleWithVirtualPublication(exercise.getInjects(), savedArticle, this.mapper);
}

@PreAuthorize("isExercisePlanner(#exerciseId)")
Expand Down Expand Up @@ -366,14 +327,7 @@ public Article createArticleForScenario(
}
});
savedArticle.setDocuments(finalArticleDocuments);
return enrichArticleWithVirtualPublication(scenario.getInjects(), savedArticle);
}

@PreAuthorize("isScenarioObserver(#scenarioId)")
@GetMapping(SCENARIO_URI + "/{scenarioId}/articles")
public Iterable<Article> scenarioArticles(@PathVariable @NotBlank final String scenarioId) {
Scenario scenario = this.scenarioService.scenario(scenarioId);
return enrichArticleWithVirtualPublication(scenario.getInjects(), scenario.getArticles());
return enrichArticleWithVirtualPublication(scenario.getInjects(), savedArticle, this.mapper);
}

@PreAuthorize("isScenarioPlanner(#scenarioId)")
Expand Down Expand Up @@ -411,7 +365,7 @@ public Article updateArticleForScenario(
});
article.setDocuments(articleDocuments);
Article savedArticle = articleRepository.save(article);
return enrichArticleWithVirtualPublication(scenario.getInjects(), savedArticle);
return enrichArticleWithVirtualPublication(scenario.getInjects(), savedArticle, this.mapper);
}

@PreAuthorize("isScenarioPlanner(#scenarioId)")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package io.openbas.rest.channel;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.openbas.database.model.Article;
import io.openbas.database.model.Inject;
import io.openbas.injectors.channel.model.ChannelContent;
import io.openbas.rest.channel.model.VirtualArticle;
import io.openbas.rest.exception.ElementNotFoundException;

import java.time.Instant;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import static io.openbas.database.model.Inject.SPEED_STANDARD;
import static io.openbas.injectors.channel.ChannelContract.CHANNEL_PUBLISH;
import static java.util.Comparator.naturalOrder;
import static java.util.Comparator.nullsFirst;

public class ChannelHelper {

private ChannelHelper() {

}

public static Article enrichArticleWithVirtualPublication(
List<Inject> injects,
Article article,
ObjectMapper mapper) {
return enrichArticleWithVirtualPublication(
injects,
List.of(article),
mapper).stream()
.findFirst()
.orElseThrow(ElementNotFoundException::new);
}

public static List<Article> enrichArticleWithVirtualPublication(
List<Inject> injects,
List<Article> articles,
ObjectMapper mapper) {
Instant now = Instant.now();
Map<String, Instant> toPublishArticleIdsMap = injects.stream()
.filter(inject -> inject.getInjectorContract().getId().equals(CHANNEL_PUBLISH))
.filter(inject -> inject.getContent() != null)
.sorted(Comparator.comparing(Inject::getDependsDuration))
.flatMap(inject -> convertToVirtualArticles(inject, now, mapper))
.filter(Objects::nonNull)
.distinct()
.collect(Collectors.toMap(VirtualArticle::id, VirtualArticle::date));
return articles.stream()
.peek(article -> article.setVirtualPublication(toPublishArticleIdsMap.get(article.getId())))
.sorted(Comparator.comparing(Article::getVirtualPublication, nullsFirst(naturalOrder()))
.thenComparing(Article::getCreatedAt)
.reversed())
.toList();
}

private static Stream<VirtualArticle> convertToVirtualArticles(Inject inject, Instant now, ObjectMapper mapper) {
Instant virtualInjectDate = inject.computeInjectDate(now, SPEED_STANDARD);
try {
ChannelContent content = mapper.treeToValue(inject.getContent(), ChannelContent.class);
return content.getArticles()
.stream()
.map(article -> new VirtualArticle(virtualInjectDate, article));
} catch (JsonProcessingException e) {
// Log the error if necessary
return Stream.empty();
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package io.openbas.rest.channel;

import io.openbas.aop.LogExecutionTime;
import io.openbas.database.model.Article;
import io.openbas.database.model.Inject;
import io.openbas.database.repository.ArticleRepository;
import io.openbas.database.repository.InjectRepository;
import io.openbas.database.specification.ArticleSpecification;
import io.openbas.database.specification.InjectSpecification;
import io.openbas.rest.channel.output.ArticleOutput;
import io.openbas.rest.helper.RestBehavior;
import jakarta.validation.constraints.NotBlank;
import lombok.RequiredArgsConstructor;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;

import static io.openbas.injectors.channel.ChannelContract.CHANNEL_PUBLISH;
import static io.openbas.rest.channel.ChannelHelper.enrichArticleWithVirtualPublication;
import static io.openbas.rest.exercise.ExerciseApi.EXERCISE_URI;

@RestController
@RequiredArgsConstructor
public class ExerciseArticleApi extends RestBehavior {

private final InjectRepository injectRepository;
private final ArticleRepository articleRepository;

@PreAuthorize("isExerciseObserver(#exerciseId)")
@GetMapping(EXERCISE_URI + "/{exerciseId}/articles")
@Transactional(readOnly = true)
@LogExecutionTime
public Iterable<ArticleOutput> exerciseArticles(@PathVariable @NotBlank final String exerciseId) {
List<Inject> injects = this.injectRepository.findAll(
InjectSpecification.fromExercise(exerciseId)
.and(InjectSpecification.fromContract(CHANNEL_PUBLISH))
);
List<Article> articles = this.articleRepository.findAll(ArticleSpecification.fromExercise(exerciseId));
return enrichArticleWithVirtualPublication(injects, articles, this.mapper)
.stream()
.map(ArticleOutput::from)
.toList();
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package io.openbas.rest.channel;

import io.openbas.aop.LogExecutionTime;
import io.openbas.database.model.Article;
import io.openbas.database.model.Inject;
import io.openbas.database.repository.ArticleRepository;
import io.openbas.database.repository.InjectRepository;
import io.openbas.database.specification.ArticleSpecification;
import io.openbas.database.specification.InjectSpecification;
import io.openbas.rest.channel.output.ArticleOutput;
import io.openbas.rest.helper.RestBehavior;
import jakarta.validation.constraints.NotBlank;
import lombok.RequiredArgsConstructor;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;

import static io.openbas.injectors.channel.ChannelContract.CHANNEL_PUBLISH;
import static io.openbas.rest.channel.ChannelHelper.enrichArticleWithVirtualPublication;
import static io.openbas.rest.scenario.ScenarioApi.SCENARIO_URI;

@RestController
@RequiredArgsConstructor
public class ScenarioArticleApi extends RestBehavior {

private final InjectRepository injectRepository;
private final ArticleRepository articleRepository;

@PreAuthorize("isScenarioObserver(#scenarioId)")
@GetMapping(SCENARIO_URI + "/{scenarioId}/articles")
@Transactional(readOnly = true)
@LogExecutionTime
public Iterable<ArticleOutput> scenarioArticles(@PathVariable @NotBlank final String scenarioId) {
List<Inject> injects = this.injectRepository.findAll(
InjectSpecification.fromScenario(scenarioId)
.and(InjectSpecification.fromContract(CHANNEL_PUBLISH))
);
List<Article> articles = this.articleRepository.findAll(ArticleSpecification.fromScenario(scenarioId));
return enrichArticleWithVirtualPublication(injects, articles, this.mapper)
.stream()
.map(ArticleOutput::from)
.toList();
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
package io.openbas.rest.channel.output;

import com.fasterxml.jackson.annotation.JsonProperty;
import io.openbas.database.model.Article;
import io.openbas.database.model.Document;
import io.openbas.database.model.Exercise;
import io.openbas.database.model.Scenario;
import jakarta.persistence.Transient;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;

import java.time.Instant;
import java.util.ArrayList;
import java.util.List;

import static java.util.Optional.ofNullable;

@Data
public class ArticleOutput {

@JsonProperty("article_id")
@NotBlank
private String id;

@JsonProperty("article_name")
private String name;

@JsonProperty("article_content")
private String content;

@JsonProperty("article_author")
private String author;

@JsonProperty("article_shares")
private Integer shares;

@JsonProperty("article_likes")
private Integer likes;

@JsonProperty("article_comments")
private Integer comments;

@JsonProperty("article_exercise")
private String exercise;

@JsonProperty("article_scenario")
private String scenario;

@JsonProperty("article_channel")
@NotBlank
private String channel;

@JsonProperty("article_documents")
private List<String> documents = new ArrayList<>();

@Transient
private Instant virtualPublication;

@JsonProperty("article_virtual_publication")
public Instant getVirtualPublication() {
return this.virtualPublication;
}

@JsonProperty("article_is_scheduled")
public boolean isScheduledPublication() {
return this.virtualPublication != null;
}

public static ArticleOutput from(@org.jetbrains.annotations.NotNull final Article article) {
ArticleOutput articleOutput = new ArticleOutput();
articleOutput.setId(article.getId());
articleOutput.setName(article.getName());
articleOutput.setContent(article.getContent());
articleOutput.setAuthor(article.getAuthor());
articleOutput.setShares(article.getShares());
articleOutput.setLikes(article.getLikes());
articleOutput.setComments(article.getComments());
articleOutput.setExercise(ofNullable(article.getExercise()).map(Exercise::getId).orElse(null));
articleOutput.setScenario(ofNullable(article.getScenario()).map(Scenario::getId).orElse(null));
articleOutput.setChannel(article.getChannel().getId());
articleOutput.setDocuments(article.getDocuments().stream().map(Document::getId).toList());
articleOutput.setVirtualPublication(article.getVirtualPublication());
return articleOutput;
}

}
Loading

0 comments on commit 379d4ac

Please sign in to comment.