Skip to content

Commit

Permalink
Merge pull request #16 from Eukon05/spring
Browse files Browse the repository at this point in the history
Added a REST API built with Spring Boot
  • Loading branch information
Eukon05 authored Nov 4, 2024
2 parents 7026846 + 9d126cd commit 8086109
Show file tree
Hide file tree
Showing 18 changed files with 263 additions and 51 deletions.
2 changes: 1 addition & 1 deletion .idea/misc.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,6 @@ default List<Article> getLatest() {
}

List<Article> getLatest(int limit);

ArticleSourceInfo getSourceInfo();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
package ovh.eukon05.infodb.api.source;

public record ArticleSourceInfo(String name, String url) {
}
45 changes: 38 additions & 7 deletions infodb-app/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,13 @@
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>ovh.eukon05</groupId>
<artifactId>infodb</artifactId>
<version>1.0-SNAPSHOT</version>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.3.5</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>


<artifactId>infodb-app</artifactId>

<dependencies>
Expand Down Expand Up @@ -40,14 +42,34 @@
</dependency>
<dependency>
<groupId>ovh.eukon05</groupId>
<artifactId>infodb-persistence-hashmap</artifactId>
<artifactId>infodb-persistence-hibernate</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>ovh.eukon05</groupId>
<artifactId>infodb-persistence-hibernate</artifactId>
<version>1.0-SNAPSHOT</version>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<version>2.3.232</version>
</dependency>
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>2.6.0</version>
</dependency>

</dependencies>

<properties>
Expand All @@ -56,4 +78,13 @@
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>

<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>

</project>
9 changes: 8 additions & 1 deletion infodb-app/src/main/java/module-info.java
Original file line number Diff line number Diff line change
@@ -1,9 +1,16 @@
import ovh.eukon05.infodb.api.persistence.ArticleDAO;
import ovh.eukon05.infodb.api.source.ArticleSource;

module ovh.eukon05.infodb.app {
open module ovh.eukon05.infodb.app {
requires ovh.eukon05.infodb.api.source;
requires ovh.eukon05.infodb.api.persistence;
requires spring.boot;
requires spring.boot.autoconfigure;
requires spring.context;
requires jakarta.persistence;
requires org.slf4j;
requires spring.web;
requires io.swagger.v3.oas.annotations;
uses ArticleSource;
uses ArticleDAO;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package ovh.eukon05.infodb.app;

import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.web.bind.annotation.*;
import ovh.eukon05.infodb.api.persistence.ArticleDAO;
import ovh.eukon05.infodb.api.persistence.ArticleDTO;
import ovh.eukon05.infodb.api.persistence.ArticleSearchCriteria;

import java.util.List;

@RestController
@RequestMapping("/api/v1/articles")
@CrossOrigin(origins = {"http://localhost:3000", "http://127.0.0.1:3000"})
@Tag(name = "Articles", description = "API methods related to fetching article data collected by infodb")
class ArticlesController {
private final ArticleDAO dao;

public ArticlesController(List<ArticleDAO> daos) {
dao = daos.get(0);
}

@GetMapping("/latest")
public List<ArticleDTO> getLatest(@RequestParam(required = false, defaultValue = "0") int page) {
return dao.getLatest(page);
}

@PostMapping
public List<ArticleDTO> search(@RequestParam(required = false, defaultValue = "0") int page, @RequestBody ArticleSearchCriteria criteria) {
return dao.findByCriteria(criteria, page);
}

@GetMapping("/{id}")
public ArticleDTO getById(@PathVariable String id) {
return dao.findById(id);
}
}
41 changes: 41 additions & 0 deletions infodb-app/src/main/java/ovh/eukon05/infodb/app/BeanConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package ovh.eukon05.infodb.app;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import ovh.eukon05.infodb.api.persistence.ArticleDAO;
import ovh.eukon05.infodb.api.source.ArticleSource;
import ovh.eukon05.infodb.api.source.ArticleSourceInfo;

import java.util.List;
import java.util.ServiceLoader;

@Configuration
class BeanConfig {

@Bean
List<ArticleDAO> articleDAOs() {
ServiceLoader<ArticleDAO> loader = ServiceLoader.load(ArticleDAO.class);

if (!loader.iterator().hasNext()) {
throw new IllegalStateException("No ArticleDAO found");
}

return loader.stream().map(ServiceLoader.Provider::get).toList();
}

@Bean
List<ArticleSource> articleSources() {
ServiceLoader<ArticleSource> loader = ServiceLoader.load(ArticleSource.class);

if (!loader.iterator().hasNext()) {
throw new IllegalStateException("No ArticleSource found");
}

return loader.stream().map(ServiceLoader.Provider::get).toList();
}

@Bean
List<ArticleSourceInfo> articleSourceInfos() {
return articleSources().stream().map(ArticleSource::getSourceInfo).toList();
}
}
37 changes: 9 additions & 28 deletions infodb-app/src/main/java/ovh/eukon05/infodb/app/Main.java
Original file line number Diff line number Diff line change
@@ -1,35 +1,16 @@
package ovh.eukon05.infodb.app;

import ovh.eukon05.infodb.api.persistence.ArticleDAO;
import ovh.eukon05.infodb.api.persistence.ArticleDTO;
import ovh.eukon05.infodb.api.persistence.ArticleSearchCriteria;
import ovh.eukon05.infodb.api.source.ArticleSource;

import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.Collections;
import java.util.ServiceLoader;
import io.swagger.v3.oas.annotations.OpenAPIDefinition;
import io.swagger.v3.oas.annotations.info.Info;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableScheduling;

@SpringBootApplication
@EnableScheduling
@OpenAPIDefinition(info = @Info(title = "infodb", description = "A REST API exposing articles regularly collected from multiple news sources", version = "v1"))
public class Main {
public static void main(String[] args) {
ServiceLoader<ArticleSource> sources = ServiceLoader.load(ArticleSource.class);
ArticleDAO dao = null;

for (ArticleDAO d : ServiceLoader.load(ArticleDAO.class)) {
if (d.getClass().getName().contains("Hibernate")) {
dao = d;
break;
}
}

for (ArticleSource source : sources) {
source.getLatest(20).stream()
.map(e -> new ArticleDTO(e.id(), e.origin(), e.title(), e.url(), e.imageUrl(), e.datePublished(), e.tags()))
.forEach(dao::save);
}

ArticleSearchCriteria criteria = new ArticleSearchCriteria(null, null, Instant.now().minus(2, ChronoUnit.HOURS), Instant.now().minus(30, ChronoUnit.MINUTES), Collections.emptyList());
dao.findByCriteria(criteria, 0).forEach(System.out::println);
//dao.getLatest(0).forEach(System.out::println);
SpringApplication.run(Main.class, args);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package ovh.eukon05.infodb.app;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import ovh.eukon05.infodb.api.persistence.ArticleDAO;
import ovh.eukon05.infodb.api.persistence.ArticleDTO;
import ovh.eukon05.infodb.api.source.Article;
import ovh.eukon05.infodb.api.source.ArticleSource;

import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.function.Function;

@Service
class PersistenceScheduler {
private static final Logger LOGGER = LoggerFactory.getLogger(PersistenceScheduler.class);
private static final Function<Article, ArticleDTO> MAPPER = article -> new ArticleDTO(article.id(), article.origin(), article.title(), article.url(), article.imageUrl(), article.datePublished(), article.tags());

private final List<ArticleSource> sources;
private final List<ArticleDAO> daos;
private final Set<String> cache = new HashSet<>();

public PersistenceScheduler(List<ArticleSource> sources, List<ArticleDAO> daos) {
this.sources = sources;
this.daos = daos;
}

@Scheduled(initialDelay = 0, fixedDelay = 300000)
private void fetchAndSave() {
LOGGER.info("Scheduled article fetch started");

sources.forEach(source -> {
LOGGER.debug("Fetching articles from source: {}", source.getClass().getSimpleName());

for (Article article : source.getLatest()) {
if (cache.contains(article.id()))
continue;

for (ArticleDAO dao : daos) {
if (dao.findById(article.id()) != null)
break;

LOGGER.debug("Saving article {} to {}", article, dao.getClass().getSimpleName());
dao.save(MAPPER.apply(article));
}
LOGGER.debug("Article {} successfully saved and cached", article.id());
cache.add(article.id());
}
});

LOGGER.info("Scheduled article fetch finished");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package ovh.eukon05.infodb.app;

import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import ovh.eukon05.infodb.api.source.ArticleSourceInfo;

import java.util.List;

@RestController
@RequestMapping("/api/v1/sources")
@CrossOrigin(origins = {"http://localhost:3000", "http://127.0.0.1:3000"})
@Tag(name = "Sources", description = "API methods exposing details about article sources used by infodb")
class SourcesController {
private final List<ArticleSourceInfo> sourceInfoList;

public SourcesController(List<ArticleSourceInfo> sourceInfoList) {
this.sourceInfoList = sourceInfoList;
}

@GetMapping
public List<ArticleSourceInfo> getSources() {
return sourceInfoList;
}
}
3 changes: 3 additions & 0 deletions infodb-app/src/main/resources/application.properties
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
logging.level.ovh.eukon05.infodb.app=INFO
springdoc.api-docs.path=/api/v1/api-docs
springdoc.swagger-ui.path=/api/v1/swagger-ui.html
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ public class HibernateDAO implements ArticleDAO {

@Override
public ArticleDTO findById(String id) {
return ArticleEntityMapper.mapFromEntity(em.find(ArticleEntity.class, id));
ArticleEntity entity = em.find(ArticleEntity.class, id);
return entity == null ? null : ArticleEntityMapper.mapFromEntity(entity);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,19 @@
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import ovh.eukon05.infodb.api.source.Article;
import ovh.eukon05.infodb.api.source.ArticleSourceInfo;

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

class DonaldArticleMapper {
private static final String BASE_URL = "https://www.donald.pl/artykuly/%s";
private static final String ORIGIN = "DONALDPL";

private DonaldArticleMapper() {
}

static Article mapFromJson(JsonObject articleDetailsJson) {
static Article mapFromJson(JsonObject articleDetailsJson, ArticleSourceInfo sourceInfo) {
String title = articleDetailsJson.get("title").getAsString();
String id = articleDetailsJson.get("uuid").getAsString();
String url = String.format(BASE_URL, id);
Expand All @@ -29,6 +29,6 @@ static Article mapFromJson(JsonObject articleDetailsJson) {
.map(e -> e.get("slug").getAsString())
.toList();

return new Article(id, ORIGIN, title, url, imageUrl, createdAt, tags);
return new Article(id, sourceInfo.name(), title, url, imageUrl, createdAt, tags);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,24 @@

import ovh.eukon05.infodb.api.source.Article;
import ovh.eukon05.infodb.api.source.ArticleSource;
import ovh.eukon05.infodb.api.source.ArticleSourceInfo;

import java.util.List;

public class DonaldSource implements ArticleSource {
private static final ArticleSourceInfo sourceInfo = new ArticleSourceInfo("DONALDPL", "https://donald.pl/news");

@Override
public List<Article> getLatest(int limit) {
return DonaldAdapter
.getLatestIds(limit)
.stream()
.map(DonaldAdapter::getArticleDetails)
.map(DonaldArticleMapper::mapFromJson).toList();
.map(json -> DonaldArticleMapper.mapFromJson(json, sourceInfo)).toList();
}

@Override
public ArticleSourceInfo getSourceInfo() {
return sourceInfo;
}
}
Loading

0 comments on commit 8086109

Please sign in to comment.