Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added a REST API built with Spring Boot #16

Merged
merged 5 commits into from
Nov 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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