diff --git a/bot/pom.xml b/bot/pom.xml index ed1d026..21aab18 100644 --- a/bot/pom.xml +++ b/bot/pom.xml @@ -128,6 +128,11 @@ kafka test + + edu.java + dto + 0.1 + diff --git a/bot/src/main/java/edu/java/bot/api/client/ScrapperClient.java b/bot/src/main/java/edu/java/bot/api/client/ScrapperClient.java deleted file mode 100644 index d6dbbec..0000000 --- a/bot/src/main/java/edu/java/bot/api/client/ScrapperClient.java +++ /dev/null @@ -1,10 +0,0 @@ -package edu.java.bot.api.client; - -import com.fasterxml.jackson.databind.ObjectMapper; -import lombok.NonNull; - -public class ScrapperClient extends AbstractClient { - public ScrapperClient(@NonNull String baseUrl, ObjectMapper mapper) { - super(baseUrl, mapper); - } -} diff --git a/bot/src/main/java/edu/java/bot/api/controller/UpdateController.java b/bot/src/main/java/edu/java/bot/api/controller/UpdateController.java index f30e7ea..e2fd998 100644 --- a/bot/src/main/java/edu/java/bot/api/controller/UpdateController.java +++ b/bot/src/main/java/edu/java/bot/api/controller/UpdateController.java @@ -1,7 +1,7 @@ package edu.java.bot.api.controller; -import edu.java.bot.api.dto.ApiErrorResponse; -import edu.java.bot.api.dto.LinkUpdateRequest; +import edu.java.dto.api.bot.ApiErrorResponse; +import edu.java.dto.api.bot.LinkUpdateRequest; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.media.Content; import io.swagger.v3.oas.annotations.media.Schema; @@ -29,5 +29,4 @@ public class UpdateController { public ResponseEntity sendUpdate(@Valid @RequestBody LinkUpdateRequest request) { return ResponseEntity.ok().build(); } - } diff --git a/bot/src/main/java/edu/java/bot/api/exception/ExceptionApiHandler.java b/bot/src/main/java/edu/java/bot/api/exception/ExceptionApiHandler.java index 31c0eeb..57cfcb0 100644 --- a/bot/src/main/java/edu/java/bot/api/exception/ExceptionApiHandler.java +++ b/bot/src/main/java/edu/java/bot/api/exception/ExceptionApiHandler.java @@ -1,13 +1,13 @@ package edu.java.bot.api.exception; -import edu.java.bot.api.dto.ApiErrorResponse; +import edu.java.dto.api.bot.ApiErrorResponse; +import java.util.Arrays; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; -@RestControllerAdvice -public class ExceptionApiHandler { +@RestControllerAdvice public class ExceptionApiHandler { @ExceptionHandler(ChatNotExistException.class) public ResponseEntity chatNotExistException(ChatNotExistException exception) { @@ -16,24 +16,19 @@ public ResponseEntity chatNotExistException(ChatNotExistExcept HttpStatus.NOT_FOUND.toString(), exception.getClass().getName(), exception.getMessage(), - exception.getStackTrace() + Arrays.stream(exception.getStackTrace()).map(StackTraceElement::toString).toArray(String[]::new) ); - return ResponseEntity - .status(HttpStatus.NOT_FOUND) - .body(error); + return ResponseEntity.status(HttpStatus.NOT_FOUND).body(error); } - @ExceptionHandler(Exception.class) - public ResponseEntity anyException(Exception exception) { + @ExceptionHandler(Exception.class) public ResponseEntity anyException(Exception exception) { ApiErrorResponse error = new ApiErrorResponse( - "Неверные параметры запроса", - HttpStatus.BAD_REQUEST.toString(), + "Произошла непредвиденная ошибка на стороне сервера", + HttpStatus.INTERNAL_SERVER_ERROR.toString(), exception.getClass().getName(), exception.getMessage(), - exception.getStackTrace() + Arrays.stream(exception.getStackTrace()).map(StackTraceElement::toString).toArray(String[]::new) ); - return ResponseEntity - .status(HttpStatus.BAD_REQUEST) - .body(error); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(error); } } diff --git a/scrapper/src/main/java/edu/java/client/AbstractClient.java b/bot/src/main/java/edu/java/bot/client/AbstractClient.java similarity index 93% rename from scrapper/src/main/java/edu/java/client/AbstractClient.java rename to bot/src/main/java/edu/java/bot/client/AbstractClient.java index 195245b..1c43082 100644 --- a/scrapper/src/main/java/edu/java/client/AbstractClient.java +++ b/bot/src/main/java/edu/java/bot/client/AbstractClient.java @@ -1,4 +1,4 @@ -package edu.java.client; +package edu.java.bot.client; import com.fasterxml.jackson.databind.ObjectMapper; import lombok.NonNull; diff --git a/bot/src/main/java/edu/java/bot/client/ScrapperApiException.java b/bot/src/main/java/edu/java/bot/client/ScrapperApiException.java new file mode 100644 index 0000000..cbb465a --- /dev/null +++ b/bot/src/main/java/edu/java/bot/client/ScrapperApiException.java @@ -0,0 +1,15 @@ +package edu.java.bot.client; + +import edu.java.dto.api.scrapper.ApiErrorResponse; +import lombok.Getter; + +@Getter +public class ScrapperApiException extends Exception { + private final ApiErrorResponse error; + + public ScrapperApiException(ApiErrorResponse error) { + super("%s: %s. %s - %s\n%s".formatted(error.code(), error.description(), + error.exceptionName(), error.exceptionMessage(), String.join("\n", error.stacktrace()))); + this.error = error; + } +} diff --git a/bot/src/main/java/edu/java/bot/client/ScrapperClient.java b/bot/src/main/java/edu/java/bot/client/ScrapperClient.java new file mode 100644 index 0000000..07037a7 --- /dev/null +++ b/bot/src/main/java/edu/java/bot/client/ScrapperClient.java @@ -0,0 +1,87 @@ +package edu.java.bot.client; + +import com.fasterxml.jackson.databind.ObjectMapper; +import edu.java.dto.api.scrapper.AddLinkRequest; +import edu.java.dto.api.scrapper.ApiErrorResponse; +import edu.java.dto.api.scrapper.LinkResponse; +import edu.java.dto.api.scrapper.ListLinksResponse; +import edu.java.dto.api.scrapper.RemoveLinkRequest; +import lombok.NonNull; +import org.springframework.http.HttpMethod; +import org.springframework.http.HttpStatusCode; +import org.springframework.http.MediaType; +import org.springframework.web.reactive.function.client.ClientResponse; +import reactor.core.publisher.Mono; + +public class ScrapperClient extends AbstractClient { + private static final String CHAT_BASE_URL = "/tg-chat"; + private static final String LINK_BASE_URL = "/links"; + private static final String ID_PATH = "/{id}"; + private static final String CHAT_HEADER = "Tg-Chat-Id"; + + public ScrapperClient(@NonNull String baseUrl, ObjectMapper mapper) { + super(baseUrl, mapper); + } + + public void registerChat(long id) { + client.post() + .uri(uriBuilder -> uriBuilder + .path(CHAT_BASE_URL + ID_PATH) + .build(id)) + .retrieve() + .onStatus(HttpStatusCode::isError, this::getException) + .bodyToMono(Void.class) + .block(); + } + + public void deleteChat(long id) { + client.delete() + .uri(uriBuilder -> uriBuilder + .path(CHAT_BASE_URL + ID_PATH) + .build(id)) + .retrieve() + .onStatus(HttpStatusCode::isError, this::getException) + .bodyToMono(Void.class) + .block(); + } + + public ListLinksResponse getLinks(long id) { + return client.get() + .uri(LINK_BASE_URL) + .header(CHAT_HEADER, String.valueOf(id)) + .retrieve() + .onStatus(HttpStatusCode::isError, this::getException) + .bodyToMono(ListLinksResponse.class) + .block(); + } + + public LinkResponse addLink(long id, AddLinkRequest request) { + return client.post() + .uri(LINK_BASE_URL) + .header(CHAT_HEADER, String.valueOf(id)) + .accept(MediaType.APPLICATION_JSON) + .body(Mono.just(request), AddLinkRequest.class) + .retrieve() + .onStatus(HttpStatusCode::isError, this::getException) + .bodyToMono(LinkResponse.class) + .block(); + } + + public LinkResponse deleteLink(long id, RemoveLinkRequest request) { + return client.method(HttpMethod.DELETE) + .uri(LINK_BASE_URL) + .header(CHAT_HEADER, String.valueOf(id)) + .accept(MediaType.APPLICATION_JSON) + .body(Mono.just(request), RemoveLinkRequest.class) + .retrieve() + .onStatus(HttpStatusCode::isError, this::getException) + .bodyToMono(LinkResponse.class) + .block(); + } + + private Mono getException(ClientResponse response) { + return response + .bodyToMono(ApiErrorResponse.class) + .map(ScrapperApiException::new); + } +} diff --git a/bot/src/main/java/edu/java/bot/service/ScrapperService.java b/bot/src/main/java/edu/java/bot/client/service/ScrapperService.java similarity index 97% rename from bot/src/main/java/edu/java/bot/service/ScrapperService.java rename to bot/src/main/java/edu/java/bot/client/service/ScrapperService.java index 1deaa2f..abbce8d 100644 --- a/bot/src/main/java/edu/java/bot/service/ScrapperService.java +++ b/bot/src/main/java/edu/java/bot/client/service/ScrapperService.java @@ -1,4 +1,4 @@ -package edu.java.bot.service; +package edu.java.bot.client.service; import java.time.LocalDateTime; import java.util.ArrayList; @@ -15,7 +15,7 @@ public ScrapperService() { public boolean isUserRegistered(Long userId) { // TODO: добавить работу с БД через scrapper log.info("%s. Проверка регистрации пользователя %d".formatted(LocalDateTime.now(), userId)); - return false; + return true; } public void registerUser(Long userId) { diff --git a/bot/src/main/java/edu/java/bot/command/Command.java b/bot/src/main/java/edu/java/bot/command/Command.java deleted file mode 100644 index 807b41a..0000000 --- a/bot/src/main/java/edu/java/bot/command/Command.java +++ /dev/null @@ -1,16 +0,0 @@ -package edu.java.bot.command; - -import com.pengrad.telegrambot.model.Update; -import edu.java.bot.exception.UnregisteredUserException; -import edu.java.bot.exception.command.CommandException; -import edu.java.bot.exception.link.LinkException; -import edu.java.bot.exception.parameter.ParameterException; - -public interface Command { - - String getName(); - - String getDescription(); - - String execute(Update update) throws UnregisteredUserException, ParameterException, CommandException, LinkException; -} diff --git a/bot/src/main/java/edu/java/bot/configuration/ClientConfig.java b/bot/src/main/java/edu/java/bot/configuration/ClientConfig.java index ccb6a51..0f74021 100644 --- a/bot/src/main/java/edu/java/bot/configuration/ClientConfig.java +++ b/bot/src/main/java/edu/java/bot/configuration/ClientConfig.java @@ -1,7 +1,7 @@ package edu.java.bot.configuration; import com.fasterxml.jackson.databind.ObjectMapper; -import edu.java.bot.api.client.ScrapperClient; +import edu.java.bot.client.ScrapperClient; import lombok.RequiredArgsConstructor; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; diff --git a/bot/src/main/java/edu/java/bot/configuration/CommandConfig.java b/bot/src/main/java/edu/java/bot/configuration/CommandConfig.java new file mode 100644 index 0000000..94af187 --- /dev/null +++ b/bot/src/main/java/edu/java/bot/configuration/CommandConfig.java @@ -0,0 +1,21 @@ +package edu.java.bot.configuration; + +import edu.java.dto.utils.LinkParser; +import edu.java.dto.utils.sources.parsers.GithubParser; +import edu.java.dto.utils.sources.parsers.SourceParser; +import edu.java.dto.utils.sources.parsers.StackoverflowParser; +import java.util.Set; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class CommandConfig { + + @Bean + public LinkParser linkParser() { + return new LinkParser(SourceParser.buildChain(Set.of( + new GithubParser(), + new StackoverflowParser() + )), true); + } +} diff --git a/bot/src/main/java/edu/java/bot/configuration/TelegramBotConfig.java b/bot/src/main/java/edu/java/bot/configuration/TelegramBotConfig.java index c4e25ce..51f91c5 100644 --- a/bot/src/main/java/edu/java/bot/configuration/TelegramBotConfig.java +++ b/bot/src/main/java/edu/java/bot/configuration/TelegramBotConfig.java @@ -3,8 +3,8 @@ import com.pengrad.telegrambot.TelegramBot; import com.pengrad.telegrambot.model.BotCommand; import com.pengrad.telegrambot.request.SetMyCommands; -import edu.java.bot.command.Command; -import edu.java.bot.command.components.HelpCommand; +import edu.java.bot.telegram.command.Command; +import edu.java.bot.telegram.command.components.HelpCommand; import java.util.List; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; diff --git a/bot/src/main/java/edu/java/bot/controller/Controller.java b/bot/src/main/java/edu/java/bot/telegram/TgBotController.java similarity index 89% rename from bot/src/main/java/edu/java/bot/controller/Controller.java rename to bot/src/main/java/edu/java/bot/telegram/TgBotController.java index d9a1c1d..8b1753a 100644 --- a/bot/src/main/java/edu/java/bot/controller/Controller.java +++ b/bot/src/main/java/edu/java/bot/telegram/TgBotController.java @@ -1,4 +1,4 @@ -package edu.java.bot.controller; +package edu.java.bot.telegram; import com.pengrad.telegrambot.TelegramBot; import com.pengrad.telegrambot.TelegramException; @@ -6,10 +6,10 @@ import com.pengrad.telegrambot.model.Update; import com.pengrad.telegrambot.request.SendMessage; import com.pengrad.telegrambot.response.SendResponse; -import edu.java.bot.command.AbstractCommand; -import edu.java.bot.command.Command; import edu.java.bot.configuration.ApplicationConfig; import edu.java.bot.configuration.TelegramBotConfig; +import edu.java.bot.telegram.command.AbstractCommand; +import edu.java.bot.telegram.command.Command; import java.time.LocalDateTime; import java.util.List; import lombok.extern.slf4j.Slf4j; @@ -18,12 +18,12 @@ @Component @Slf4j -public class Controller { +public class TgBotController { private final TelegramBot bot; private final TelegramBotConfig botConfig; @Autowired - public Controller(ApplicationConfig config, TelegramBotConfig botConfig) { + public TgBotController(ApplicationConfig config, TelegramBotConfig botConfig) { this.botConfig = botConfig; bot = botConfig.telegramBot(config); bot.setUpdatesListener(this::processUpdates, this::processException); diff --git a/bot/src/main/java/edu/java/bot/command/AbstractCommand.java b/bot/src/main/java/edu/java/bot/telegram/command/AbstractCommand.java similarity index 74% rename from bot/src/main/java/edu/java/bot/command/AbstractCommand.java rename to bot/src/main/java/edu/java/bot/telegram/command/AbstractCommand.java index 6d1d6e6..9b4f36b 100644 --- a/bot/src/main/java/edu/java/bot/command/AbstractCommand.java +++ b/bot/src/main/java/edu/java/bot/telegram/command/AbstractCommand.java @@ -1,12 +1,12 @@ -package edu.java.bot.command; +package edu.java.bot.telegram.command; import com.pengrad.telegrambot.model.Update; -import edu.java.bot.exception.UnregisteredUserException; -import edu.java.bot.exception.command.CommandException; -import edu.java.bot.exception.command.CommandNotExistException; -import edu.java.bot.exception.command.NotCommandException; -import edu.java.bot.exception.link.LinkException; -import edu.java.bot.exception.parameter.ParameterException; +import edu.java.bot.telegram.exception.UnregisteredUserException; +import edu.java.bot.telegram.exception.command.CommandException; +import edu.java.bot.telegram.exception.command.CommandNotExistException; +import edu.java.bot.telegram.exception.command.NotCommandException; +import edu.java.bot.telegram.exception.parameter.ParameterException; +import edu.java.dto.utils.exception.LinkException; import java.util.ArrayList; import java.util.Arrays; import java.util.List; diff --git a/bot/src/main/java/edu/java/bot/command/AbstractServiceCommand.java b/bot/src/main/java/edu/java/bot/telegram/command/AbstractServiceCommand.java similarity index 77% rename from bot/src/main/java/edu/java/bot/command/AbstractServiceCommand.java rename to bot/src/main/java/edu/java/bot/telegram/command/AbstractServiceCommand.java index 028ee0b..a78798c 100644 --- a/bot/src/main/java/edu/java/bot/command/AbstractServiceCommand.java +++ b/bot/src/main/java/edu/java/bot/telegram/command/AbstractServiceCommand.java @@ -1,6 +1,6 @@ -package edu.java.bot.command; +package edu.java.bot.telegram.command; -import edu.java.bot.service.ScrapperService; +import edu.java.bot.client.service.ScrapperService; public abstract class AbstractServiceCommand extends AbstractCommand { protected ScrapperService service; diff --git a/bot/src/main/java/edu/java/bot/telegram/command/Command.java b/bot/src/main/java/edu/java/bot/telegram/command/Command.java new file mode 100644 index 0000000..874b01e --- /dev/null +++ b/bot/src/main/java/edu/java/bot/telegram/command/Command.java @@ -0,0 +1,16 @@ +package edu.java.bot.telegram.command; + +import com.pengrad.telegrambot.model.Update; +import edu.java.bot.telegram.exception.UnregisteredUserException; +import edu.java.bot.telegram.exception.command.CommandException; +import edu.java.bot.telegram.exception.parameter.ParameterException; +import edu.java.dto.utils.exception.LinkException; + +public interface Command { + + String getName(); + + String getDescription(); + + String execute(Update update) throws UnregisteredUserException, ParameterException, CommandException, LinkException; +} diff --git a/bot/src/main/java/edu/java/bot/command/CommandUtils.java b/bot/src/main/java/edu/java/bot/telegram/command/CommandUtils.java similarity index 72% rename from bot/src/main/java/edu/java/bot/command/CommandUtils.java rename to bot/src/main/java/edu/java/bot/telegram/command/CommandUtils.java index 666fc21..3eb2d27 100644 --- a/bot/src/main/java/edu/java/bot/command/CommandUtils.java +++ b/bot/src/main/java/edu/java/bot/telegram/command/CommandUtils.java @@ -1,8 +1,8 @@ -package edu.java.bot.command; +package edu.java.bot.telegram.command; -import edu.java.bot.exception.UnregisteredUserException; -import edu.java.bot.exception.parameter.WrongNumberParametersException; -import edu.java.bot.service.ScrapperService; +import edu.java.bot.client.service.ScrapperService; +import edu.java.bot.telegram.exception.UnregisteredUserException; +import edu.java.bot.telegram.exception.parameter.WrongNumberParametersException; public class CommandUtils { diff --git a/bot/src/main/java/edu/java/bot/command/components/HelpCommand.java b/bot/src/main/java/edu/java/bot/telegram/command/components/HelpCommand.java similarity index 76% rename from bot/src/main/java/edu/java/bot/command/components/HelpCommand.java rename to bot/src/main/java/edu/java/bot/telegram/command/components/HelpCommand.java index 6360263..b0a1895 100644 --- a/bot/src/main/java/edu/java/bot/command/components/HelpCommand.java +++ b/bot/src/main/java/edu/java/bot/telegram/command/components/HelpCommand.java @@ -1,9 +1,9 @@ -package edu.java.bot.command.components; +package edu.java.bot.telegram.command.components; -import edu.java.bot.command.AbstractCommand; -import edu.java.bot.command.Command; -import edu.java.bot.command.CommandUtils; -import edu.java.bot.exception.parameter.ParameterException; +import edu.java.bot.telegram.command.AbstractCommand; +import edu.java.bot.telegram.command.Command; +import edu.java.bot.telegram.command.CommandUtils; +import edu.java.bot.telegram.exception.parameter.ParameterException; import java.util.List; import java.util.stream.Collectors; import org.springframework.core.annotation.Order; diff --git a/bot/src/main/java/edu/java/bot/command/components/ListCommand.java b/bot/src/main/java/edu/java/bot/telegram/command/components/ListCommand.java similarity index 77% rename from bot/src/main/java/edu/java/bot/command/components/ListCommand.java rename to bot/src/main/java/edu/java/bot/telegram/command/components/ListCommand.java index 1f7a377..1f6cd78 100644 --- a/bot/src/main/java/edu/java/bot/command/components/ListCommand.java +++ b/bot/src/main/java/edu/java/bot/telegram/command/components/ListCommand.java @@ -1,10 +1,10 @@ -package edu.java.bot.command.components; +package edu.java.bot.telegram.command.components; -import edu.java.bot.command.AbstractServiceCommand; -import edu.java.bot.command.CommandUtils; -import edu.java.bot.exception.UnregisteredUserException; -import edu.java.bot.exception.parameter.ParameterException; -import edu.java.bot.service.ScrapperService; +import edu.java.bot.client.service.ScrapperService; +import edu.java.bot.telegram.command.AbstractServiceCommand; +import edu.java.bot.telegram.command.CommandUtils; +import edu.java.bot.telegram.exception.UnregisteredUserException; +import edu.java.bot.telegram.exception.parameter.ParameterException; import java.util.List; import java.util.stream.Collectors; import org.springframework.core.annotation.Order; diff --git a/bot/src/main/java/edu/java/bot/command/components/StartCommand.java b/bot/src/main/java/edu/java/bot/telegram/command/components/StartCommand.java similarity index 77% rename from bot/src/main/java/edu/java/bot/command/components/StartCommand.java rename to bot/src/main/java/edu/java/bot/telegram/command/components/StartCommand.java index 120ae79..e4f0af0 100644 --- a/bot/src/main/java/edu/java/bot/command/components/StartCommand.java +++ b/bot/src/main/java/edu/java/bot/telegram/command/components/StartCommand.java @@ -1,9 +1,9 @@ -package edu.java.bot.command.components; +package edu.java.bot.telegram.command.components; -import edu.java.bot.command.AbstractServiceCommand; -import edu.java.bot.command.CommandUtils; -import edu.java.bot.exception.parameter.ParameterException; -import edu.java.bot.service.ScrapperService; +import edu.java.bot.client.service.ScrapperService; +import edu.java.bot.telegram.command.AbstractServiceCommand; +import edu.java.bot.telegram.command.CommandUtils; +import edu.java.bot.telegram.exception.parameter.ParameterException; import org.springframework.core.annotation.Order; import org.springframework.stereotype.Component; diff --git a/bot/src/main/java/edu/java/bot/command/components/TrackCommand.java b/bot/src/main/java/edu/java/bot/telegram/command/components/TrackCommand.java similarity index 55% rename from bot/src/main/java/edu/java/bot/command/components/TrackCommand.java rename to bot/src/main/java/edu/java/bot/telegram/command/components/TrackCommand.java index 466f995..09a061c 100644 --- a/bot/src/main/java/edu/java/bot/command/components/TrackCommand.java +++ b/bot/src/main/java/edu/java/bot/telegram/command/components/TrackCommand.java @@ -1,22 +1,24 @@ -package edu.java.bot.command.components; +package edu.java.bot.telegram.command.components; -import edu.java.bot.command.AbstractServiceCommand; -import edu.java.bot.command.CommandUtils; -import edu.java.bot.exception.UnregisteredUserException; -import edu.java.bot.exception.link.LinkRegistrationException; -import edu.java.bot.exception.link.NotLinkException; -import edu.java.bot.exception.parameter.ParameterException; -import edu.java.bot.service.ScrapperService; -import edu.java.bot.utils.Link; +import edu.java.bot.client.service.ScrapperService; +import edu.java.bot.telegram.command.AbstractServiceCommand; +import edu.java.bot.telegram.command.CommandUtils; +import edu.java.bot.telegram.exception.UnregisteredUserException; +import edu.java.bot.telegram.exception.parameter.ParameterException; +import edu.java.dto.utils.LinkParser; +import edu.java.dto.utils.exception.LinkException; +import edu.java.dto.utils.exception.LinkRegistrationException; import org.springframework.core.annotation.Order; import org.springframework.stereotype.Component; @Component @Order(3) public class TrackCommand extends AbstractServiceCommand { + private final LinkParser parser; - public TrackCommand(ScrapperService service) { + public TrackCommand(ScrapperService service, LinkParser parser) { super(service); + this.parser = parser; } @Override @@ -31,10 +33,10 @@ public String getDescription() { @Override protected String doAction(Long userId, String[] params) - throws ParameterException, NotLinkException, UnregisteredUserException, LinkRegistrationException { + throws ParameterException, UnregisteredUserException, LinkException { CommandUtils.checkParamsNumber(params, 1); String link = params[0]; - Link.parse(link); + parser.parse(link); CommandUtils.checkUserRegistration(userId, service); if (service.isLinkRegistered(userId, link)) { throw new LinkRegistrationException("Ссылка уже зарегистрирована"); diff --git a/bot/src/main/java/edu/java/bot/command/components/UntrackCommand.java b/bot/src/main/java/edu/java/bot/telegram/command/components/UntrackCommand.java similarity index 56% rename from bot/src/main/java/edu/java/bot/command/components/UntrackCommand.java rename to bot/src/main/java/edu/java/bot/telegram/command/components/UntrackCommand.java index 3e65102..c828b4f 100644 --- a/bot/src/main/java/edu/java/bot/command/components/UntrackCommand.java +++ b/bot/src/main/java/edu/java/bot/telegram/command/components/UntrackCommand.java @@ -1,22 +1,24 @@ -package edu.java.bot.command.components; +package edu.java.bot.telegram.command.components; -import edu.java.bot.command.AbstractServiceCommand; -import edu.java.bot.command.CommandUtils; -import edu.java.bot.exception.UnregisteredUserException; -import edu.java.bot.exception.link.LinkRegistrationException; -import edu.java.bot.exception.link.NotLinkException; -import edu.java.bot.exception.parameter.ParameterException; -import edu.java.bot.service.ScrapperService; -import edu.java.bot.utils.Link; +import edu.java.bot.client.service.ScrapperService; +import edu.java.bot.telegram.command.AbstractServiceCommand; +import edu.java.bot.telegram.command.CommandUtils; +import edu.java.bot.telegram.exception.UnregisteredUserException; +import edu.java.bot.telegram.exception.parameter.ParameterException; +import edu.java.dto.utils.LinkParser; +import edu.java.dto.utils.exception.LinkException; +import edu.java.dto.utils.exception.LinkRegistrationException; import org.springframework.core.annotation.Order; import org.springframework.stereotype.Component; @Component @Order(4) public class UntrackCommand extends AbstractServiceCommand { + private final LinkParser parser; - public UntrackCommand(ScrapperService service) { + public UntrackCommand(ScrapperService service, LinkParser parser) { super(service); + this.parser = parser; } @Override @@ -31,10 +33,10 @@ public String getDescription() { @Override protected String doAction(Long userId, String[] params) - throws ParameterException, NotLinkException, UnregisteredUserException, LinkRegistrationException { + throws ParameterException, UnregisteredUserException, LinkException { CommandUtils.checkParamsNumber(params, 1); String link = params[0]; - Link.parse(link); + parser.parse(link); CommandUtils.checkUserRegistration(userId, service); if (!service.isLinkRegistered(userId, link)) { throw new LinkRegistrationException("Ссылка не была зарегистрирована"); diff --git a/bot/src/main/java/edu/java/bot/exception/UnregisteredUserException.java b/bot/src/main/java/edu/java/bot/telegram/exception/UnregisteredUserException.java similarity index 84% rename from bot/src/main/java/edu/java/bot/exception/UnregisteredUserException.java rename to bot/src/main/java/edu/java/bot/telegram/exception/UnregisteredUserException.java index c8760df..3f94199 100644 --- a/bot/src/main/java/edu/java/bot/exception/UnregisteredUserException.java +++ b/bot/src/main/java/edu/java/bot/telegram/exception/UnregisteredUserException.java @@ -1,4 +1,4 @@ -package edu.java.bot.exception; +package edu.java.bot.telegram.exception; public class UnregisteredUserException extends Exception { diff --git a/bot/src/main/java/edu/java/bot/exception/command/CommandException.java b/bot/src/main/java/edu/java/bot/telegram/exception/command/CommandException.java similarity index 73% rename from bot/src/main/java/edu/java/bot/exception/command/CommandException.java rename to bot/src/main/java/edu/java/bot/telegram/exception/command/CommandException.java index 2bc19fe..25f1725 100644 --- a/bot/src/main/java/edu/java/bot/exception/command/CommandException.java +++ b/bot/src/main/java/edu/java/bot/telegram/exception/command/CommandException.java @@ -1,4 +1,4 @@ -package edu.java.bot.exception.command; +package edu.java.bot.telegram.exception.command; public abstract class CommandException extends Exception { diff --git a/bot/src/main/java/edu/java/bot/exception/command/CommandNotExistException.java b/bot/src/main/java/edu/java/bot/telegram/exception/command/CommandNotExistException.java similarity index 77% rename from bot/src/main/java/edu/java/bot/exception/command/CommandNotExistException.java rename to bot/src/main/java/edu/java/bot/telegram/exception/command/CommandNotExistException.java index 6e6c488..e73c5b9 100644 --- a/bot/src/main/java/edu/java/bot/exception/command/CommandNotExistException.java +++ b/bot/src/main/java/edu/java/bot/telegram/exception/command/CommandNotExistException.java @@ -1,4 +1,4 @@ -package edu.java.bot.exception.command; +package edu.java.bot.telegram.exception.command; public class CommandNotExistException extends CommandException { public CommandNotExistException() { diff --git a/bot/src/main/java/edu/java/bot/exception/command/NotCommandException.java b/bot/src/main/java/edu/java/bot/telegram/exception/command/NotCommandException.java similarity index 76% rename from bot/src/main/java/edu/java/bot/exception/command/NotCommandException.java rename to bot/src/main/java/edu/java/bot/telegram/exception/command/NotCommandException.java index 65def43..e1c5919 100644 --- a/bot/src/main/java/edu/java/bot/exception/command/NotCommandException.java +++ b/bot/src/main/java/edu/java/bot/telegram/exception/command/NotCommandException.java @@ -1,4 +1,4 @@ -package edu.java.bot.exception.command; +package edu.java.bot.telegram.exception.command; public class NotCommandException extends CommandException { public NotCommandException() { diff --git a/bot/src/main/java/edu/java/bot/exception/parameter/ParameterException.java b/bot/src/main/java/edu/java/bot/telegram/exception/parameter/ParameterException.java similarity index 73% rename from bot/src/main/java/edu/java/bot/exception/parameter/ParameterException.java rename to bot/src/main/java/edu/java/bot/telegram/exception/parameter/ParameterException.java index 017af07..5cb975c 100644 --- a/bot/src/main/java/edu/java/bot/exception/parameter/ParameterException.java +++ b/bot/src/main/java/edu/java/bot/telegram/exception/parameter/ParameterException.java @@ -1,4 +1,4 @@ -package edu.java.bot.exception.parameter; +package edu.java.bot.telegram.exception.parameter; public abstract class ParameterException extends Exception { diff --git a/bot/src/main/java/edu/java/bot/exception/parameter/WrongNumberParametersException.java b/bot/src/main/java/edu/java/bot/telegram/exception/parameter/WrongNumberParametersException.java similarity index 76% rename from bot/src/main/java/edu/java/bot/exception/parameter/WrongNumberParametersException.java rename to bot/src/main/java/edu/java/bot/telegram/exception/parameter/WrongNumberParametersException.java index f9c4147..9b4417f 100644 --- a/bot/src/main/java/edu/java/bot/exception/parameter/WrongNumberParametersException.java +++ b/bot/src/main/java/edu/java/bot/telegram/exception/parameter/WrongNumberParametersException.java @@ -1,4 +1,4 @@ -package edu.java.bot.exception.parameter; +package edu.java.bot.telegram.exception.parameter; public class WrongNumberParametersException extends ParameterException { diff --git a/bot/src/main/java/edu/java/bot/utils/Link.java b/bot/src/main/java/edu/java/bot/utils/Link.java deleted file mode 100644 index 0421d92..0000000 --- a/bot/src/main/java/edu/java/bot/utils/Link.java +++ /dev/null @@ -1,24 +0,0 @@ -package edu.java.bot.utils; - -import edu.java.bot.exception.link.NotLinkException; -import java.net.MalformedURLException; -import java.net.URI; -import java.net.URISyntaxException; -import java.net.URL; - -public class Link { - private Link() { - } - - public static URL parse(String link) throws NotLinkException { - String url = link.toLowerCase(); - if (!url.startsWith("http://") && !url.startsWith("https://")) { - throw new NotLinkException("У ссылки отсутствует протокол"); - } - try { - return new URI(link).toURL(); - } catch (MalformedURLException | URISyntaxException e) { - throw new NotLinkException("Неверная ссылка"); - } - } -} diff --git a/bot/src/test/java/edu/java/bot/BotApplicationTest.java b/bot/src/test/java/edu/java/bot/BotApplicationTest.java index 65f3332..51bea96 100644 --- a/bot/src/test/java/edu/java/bot/BotApplicationTest.java +++ b/bot/src/test/java/edu/java/bot/BotApplicationTest.java @@ -3,20 +3,22 @@ import com.pengrad.telegrambot.model.Message; import com.pengrad.telegrambot.model.Update; import com.pengrad.telegrambot.model.User; -import edu.java.bot.command.AbstractCommand; -import edu.java.bot.command.AbstractServiceCommand; -import edu.java.bot.command.Command; +import edu.java.bot.telegram.command.AbstractCommand; +import edu.java.bot.telegram.command.AbstractServiceCommand; +import edu.java.bot.telegram.command.Command; import edu.java.bot.configuration.TelegramBotConfig; -import edu.java.bot.exception.UnregisteredUserException; -import edu.java.bot.exception.command.CommandException; -import edu.java.bot.exception.command.CommandNotExistException; -import edu.java.bot.exception.command.NotCommandException; -import edu.java.bot.exception.link.LinkException; -import edu.java.bot.exception.link.LinkRegistrationException; -import edu.java.bot.exception.link.NotLinkException; -import edu.java.bot.exception.parameter.ParameterException; -import edu.java.bot.exception.parameter.WrongNumberParametersException; +import edu.java.bot.telegram.exception.UnregisteredUserException; +import edu.java.bot.telegram.exception.command.CommandException; +import edu.java.bot.telegram.exception.command.CommandNotExistException; +import edu.java.bot.telegram.exception.command.NotCommandException; +import edu.java.dto.utils.exception.BadSourceUrlException; +import edu.java.dto.utils.exception.LinkException; +import edu.java.dto.utils.exception.LinkRegistrationException; +import edu.java.dto.utils.exception.NotLinkException; +import edu.java.bot.telegram.exception.parameter.ParameterException; +import edu.java.bot.telegram.exception.parameter.WrongNumberParametersException; import java.util.List; +import edu.java.dto.utils.exception.SourceNotSupportedException; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -122,10 +124,10 @@ void helpTest() throws UnregisteredUserException, ParameterException, CommandExc void trackTest() throws UnregisteredUserException, ParameterException, CommandException, LinkException { service.setUserRegistered(true); service.setLinks(List.of()); - setMessage("/track https://github.com/cyberpanncake"); + setMessage("/track https://github.com/cyberpanncake/Torzhkova-Tinkoff-JavaBackendCourse-Spring"); Command command = AbstractCommand.parse(updateMock, config.commands()); String expected = "Ссылка добавлена в отслеживаемые"; - String actual = command.execute(updateMock); + String actual = command.execute(updateMock); Assertions.assertEquals(expected, actual); } @@ -138,18 +140,36 @@ void notLinkExceptionTest() throws CommandException { Assertions.assertThrows(NotLinkException.class, () -> command.execute(updateMock)); } + @Test + void sourceNotSupportedExceptionTest() throws CommandException { + service.setUserRegistered(true); + service.setLinks(List.of()); + setMessage("/track https://google.com"); + Command command = AbstractCommand.parse(updateMock, config.commands()); + Assertions.assertThrows(SourceNotSupportedException.class, () -> command.execute(updateMock)); + } + + @Test + void badSourceUrlExceptionTest() throws CommandException { + service.setUserRegistered(true); + service.setLinks(List.of()); + setMessage("/track https://github.com/cyberpanncake"); + Command command = AbstractCommand.parse(updateMock, config.commands()); + Assertions.assertThrows(BadSourceUrlException.class, () -> command.execute(updateMock)); + } + @Test void unregisteredUserExceptionTest() throws CommandException { service.setUserRegistered(false); service.setLinks(List.of()); - setMessage("/track https://github.com/cyberpanncake"); + setMessage("/track https://github.com/cyberpanncake/Torzhkova-Tinkoff-JavaBackendCourse-Spring"); Command command = AbstractCommand.parse(updateMock, config.commands()); Assertions.assertThrows(UnregisteredUserException.class, () -> command.execute(updateMock)); } @Test void linkRegisteredExceptionTest() throws CommandException { - String link = "https://github.com/cyberpanncake"; + String link = "https://github.com/cyberpanncake/Torzhkova-Tinkoff-JavaBackendCourse-Spring"; service.setUserRegistered(true); service.setLinks(List.of(link)); setMessage("/track " + link); @@ -159,7 +179,7 @@ void linkRegisteredExceptionTest() throws CommandException { @Test void untrackTest() throws UnregisteredUserException, ParameterException, CommandException, LinkException { - String link = "https://github.com/cyberpanncake"; + String link = "https://github.com/cyberpanncake/Torzhkova-Tinkoff-JavaBackendCourse-Spring"; service.setUserRegistered(true); service.setLinks(List.of(link)); setMessage("/untrack " + link); @@ -173,7 +193,7 @@ void untrackTest() throws UnregisteredUserException, ParameterException, Command void linkNotRegisteredExceptionTest() throws CommandException { service.setUserRegistered(true); service.setLinks(List.of()); - setMessage("/untrack https://github.com/cyberpanncake"); + setMessage("/untrack https://github.com/cyberpanncake/Torzhkova-Tinkoff-JavaBackendCourse-Spring"); Command command = AbstractCommand.parse(updateMock, config.commands()); Assertions.assertThrows(LinkRegistrationException.class, () -> command.execute(updateMock)); } diff --git a/bot/src/test/java/edu/java/bot/TestScrapperService.java b/bot/src/test/java/edu/java/bot/TestScrapperService.java index 6eb3f3c..b5a827c 100644 --- a/bot/src/test/java/edu/java/bot/TestScrapperService.java +++ b/bot/src/test/java/edu/java/bot/TestScrapperService.java @@ -1,6 +1,6 @@ package edu.java.bot; -import edu.java.bot.service.ScrapperService; +import edu.java.bot.client.service.ScrapperService; import java.util.List; import lombok.Setter; diff --git a/dto/pom.xml b/dto/pom.xml new file mode 100644 index 0000000..a9c9b8f --- /dev/null +++ b/dto/pom.xml @@ -0,0 +1,33 @@ + + + 4.0.0 + + edu.java + root + ${revision} + + + dto + ${revision} + + + + com.fasterxml.jackson.core + jackson-databind + 2.15.3 + + + org.springframework.boot + spring-boot-starter-validation + + + + + 21 + 21 + UTF-8 + + + diff --git a/scrapper/src/main/java/edu/java/api/dto/ApiErrorResponse.java b/dto/src/main/java/edu/java/dto/api/bot/ApiErrorResponse.java similarity index 79% rename from scrapper/src/main/java/edu/java/api/dto/ApiErrorResponse.java rename to dto/src/main/java/edu/java/dto/api/bot/ApiErrorResponse.java index e04ccab..c760ecd 100644 --- a/scrapper/src/main/java/edu/java/api/dto/ApiErrorResponse.java +++ b/dto/src/main/java/edu/java/dto/api/bot/ApiErrorResponse.java @@ -1,4 +1,4 @@ -package edu.java.api.dto; +package edu.java.dto.api.bot; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; @@ -8,7 +8,6 @@ public record ApiErrorResponse( String code, String exceptionName, String exceptionMessage, - - StackTraceElement[] stacktrace + String[] stacktrace ) { } diff --git a/bot/src/main/java/edu/java/bot/api/dto/LinkUpdateRequest.java b/dto/src/main/java/edu/java/dto/api/bot/LinkUpdateRequest.java similarity index 55% rename from bot/src/main/java/edu/java/bot/api/dto/LinkUpdateRequest.java rename to dto/src/main/java/edu/java/dto/api/bot/LinkUpdateRequest.java index 3598f86..410fa0d 100644 --- a/bot/src/main/java/edu/java/bot/api/dto/LinkUpdateRequest.java +++ b/dto/src/main/java/edu/java/dto/api/bot/LinkUpdateRequest.java @@ -1,14 +1,14 @@ -package edu.java.bot.api.dto; +package edu.java.dto.api.bot; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import org.hibernate.validator.constraints.URL; +import java.net.URI; @JsonIgnoreProperties(ignoreUnknown = true) public record LinkUpdateRequest( - long id, - @URL - String url, + Long id, + URI url, String description, - long[] tgChatIds + Long[] tgChatIds ) { } + diff --git a/scrapper/src/main/java/edu/java/api/dto/AddLinkRequest.java b/dto/src/main/java/edu/java/dto/api/scrapper/AddLinkRequest.java similarity index 86% rename from scrapper/src/main/java/edu/java/api/dto/AddLinkRequest.java rename to dto/src/main/java/edu/java/dto/api/scrapper/AddLinkRequest.java index 35e5038..a45ba0c 100644 --- a/scrapper/src/main/java/edu/java/api/dto/AddLinkRequest.java +++ b/dto/src/main/java/edu/java/dto/api/scrapper/AddLinkRequest.java @@ -1,4 +1,4 @@ -package edu.java.api.dto; +package edu.java.dto.api.scrapper; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import org.hibernate.validator.constraints.URL; diff --git a/bot/src/main/java/edu/java/bot/api/dto/ApiErrorResponse.java b/dto/src/main/java/edu/java/dto/api/scrapper/ApiErrorResponse.java similarity index 78% rename from bot/src/main/java/edu/java/bot/api/dto/ApiErrorResponse.java rename to dto/src/main/java/edu/java/dto/api/scrapper/ApiErrorResponse.java index 364072c..47752f1 100644 --- a/bot/src/main/java/edu/java/bot/api/dto/ApiErrorResponse.java +++ b/dto/src/main/java/edu/java/dto/api/scrapper/ApiErrorResponse.java @@ -1,4 +1,4 @@ -package edu.java.bot.api.dto; +package edu.java.dto.api.scrapper; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; @@ -8,7 +8,6 @@ public record ApiErrorResponse( String code, String exceptionName, String exceptionMessage, - - StackTraceElement[] stacktrace + String[] stacktrace ) { } diff --git a/scrapper/src/main/java/edu/java/api/dto/LinkResponse.java b/dto/src/main/java/edu/java/dto/api/scrapper/LinkResponse.java similarity index 56% rename from scrapper/src/main/java/edu/java/api/dto/LinkResponse.java rename to dto/src/main/java/edu/java/dto/api/scrapper/LinkResponse.java index ce13424..bf3d094 100644 --- a/scrapper/src/main/java/edu/java/api/dto/LinkResponse.java +++ b/dto/src/main/java/edu/java/dto/api/scrapper/LinkResponse.java @@ -1,12 +1,11 @@ -package edu.java.api.dto; +package edu.java.dto.api.scrapper; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import org.hibernate.validator.constraints.URL; +import java.net.URI; @JsonIgnoreProperties(ignoreUnknown = true) public record LinkResponse( - long id, - @URL - String url + Long id, + URI url ) { } diff --git a/scrapper/src/main/java/edu/java/api/dto/ListLinksResponse.java b/dto/src/main/java/edu/java/dto/api/scrapper/ListLinksResponse.java similarity index 76% rename from scrapper/src/main/java/edu/java/api/dto/ListLinksResponse.java rename to dto/src/main/java/edu/java/dto/api/scrapper/ListLinksResponse.java index a6707c5..6393dce 100644 --- a/scrapper/src/main/java/edu/java/api/dto/ListLinksResponse.java +++ b/dto/src/main/java/edu/java/dto/api/scrapper/ListLinksResponse.java @@ -1,10 +1,10 @@ -package edu.java.api.dto; +package edu.java.dto.api.scrapper; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; @JsonIgnoreProperties(ignoreUnknown = true) public record ListLinksResponse( LinkResponse[] links, - int size + Integer size ) { } diff --git a/scrapper/src/main/java/edu/java/api/dto/RemoveLinkRequest.java b/dto/src/main/java/edu/java/dto/api/scrapper/RemoveLinkRequest.java similarity index 86% rename from scrapper/src/main/java/edu/java/api/dto/RemoveLinkRequest.java rename to dto/src/main/java/edu/java/dto/api/scrapper/RemoveLinkRequest.java index 318f8f2..1ed7b78 100644 --- a/scrapper/src/main/java/edu/java/api/dto/RemoveLinkRequest.java +++ b/dto/src/main/java/edu/java/dto/api/scrapper/RemoveLinkRequest.java @@ -1,4 +1,4 @@ -package edu.java.api.dto; +package edu.java.dto.api.scrapper; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import org.hibernate.validator.constraints.URL; diff --git a/dto/src/main/java/edu/java/dto/utils/LinkInfo.java b/dto/src/main/java/edu/java/dto/utils/LinkInfo.java new file mode 100644 index 0000000..f024812 --- /dev/null +++ b/dto/src/main/java/edu/java/dto/utils/LinkInfo.java @@ -0,0 +1,7 @@ +package edu.java.dto.utils; + +import edu.java.dto.utils.sources.info.SourceInfo; +import java.net.URI; + +public record LinkInfo(URI url, SourceInfo source) { +} diff --git a/dto/src/main/java/edu/java/dto/utils/LinkParser.java b/dto/src/main/java/edu/java/dto/utils/LinkParser.java new file mode 100644 index 0000000..b1e2eef --- /dev/null +++ b/dto/src/main/java/edu/java/dto/utils/LinkParser.java @@ -0,0 +1,73 @@ +package edu.java.dto.utils; + +import edu.java.dto.utils.exception.NotLinkException; +import edu.java.dto.utils.exception.SourceException; +import edu.java.dto.utils.exception.SourceNotSupportedException; +import edu.java.dto.utils.sources.info.SourceInfo; +import edu.java.dto.utils.sources.parsers.SourceParser; +import java.net.HttpURLConnection; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URL; + +public class LinkParser { + private static final String NOT_LINK_MESSAGE = "Неверная ссылка"; + private final SourceParser parser; + private final boolean checkIsLinkAvailable; + + public LinkParser(boolean checkLinkAvailable) { + this.checkIsLinkAvailable = checkLinkAvailable; + parser = null; + } + + public LinkParser(SourceParser parser, boolean checkIsLinkAvailable) { + this.parser = parser; + this.checkIsLinkAvailable = checkIsLinkAvailable; + } + + public LinkInfo parse(String link) throws NotLinkException, SourceException { + URI uri = tryMakeURI(link); + if (checkIsLinkAvailable) { + checkIsLinkAvailable(uri); + } + SourceInfo sourceInfo = tryParseSource(uri); + return new LinkInfo(uri, sourceInfo); + } + + private static URI tryMakeURI(String link) throws NotLinkException { + try { + return new URI(link); + } catch (URISyntaxException e) { + throw new NotLinkException(NOT_LINK_MESSAGE); + } + } + + private static void checkIsLinkAvailable(URI uri) throws NotLinkException { + HttpURLConnection connection = null; + try { + URL url = uri.toURL(); + connection = (HttpURLConnection) url.openConnection(); + connection.setRequestMethod("HEAD"); + int response = connection.getResponseCode(); + if (response != HttpURLConnection.HTTP_OK) { + throw new NotLinkException("Ссылка не существует или недоступна"); + } + } catch (NotLinkException e) { + throw e; + } catch (Exception e) { + throw new NotLinkException(NOT_LINK_MESSAGE); + } finally { + if (connection != null) { + connection.disconnect(); + } + } + } + + private SourceInfo tryParseSource(URI uri) throws SourceException { + try { + return parser.parse(uri); + } catch (NullPointerException | SourceNotSupportedException e) { + throw new SourceNotSupportedException("Данный ресурс не поддерживается"); + } + } +} diff --git a/dto/src/main/java/edu/java/dto/utils/exception/BadSourceUrlException.java b/dto/src/main/java/edu/java/dto/utils/exception/BadSourceUrlException.java new file mode 100644 index 0000000..21bc38a --- /dev/null +++ b/dto/src/main/java/edu/java/dto/utils/exception/BadSourceUrlException.java @@ -0,0 +1,10 @@ +package edu.java.dto.utils.exception; + +public class BadSourceUrlException extends SourceException { + public BadSourceUrlException() { + } + + public BadSourceUrlException(String message) { + super(message); + } +} diff --git a/bot/src/main/java/edu/java/bot/exception/link/LinkException.java b/dto/src/main/java/edu/java/dto/utils/exception/LinkException.java similarity index 64% rename from bot/src/main/java/edu/java/bot/exception/link/LinkException.java rename to dto/src/main/java/edu/java/dto/utils/exception/LinkException.java index 483556e..7a005b5 100644 --- a/bot/src/main/java/edu/java/bot/exception/link/LinkException.java +++ b/dto/src/main/java/edu/java/dto/utils/exception/LinkException.java @@ -1,6 +1,8 @@ -package edu.java.bot.exception.link; +package edu.java.dto.utils.exception; public abstract class LinkException extends Exception { + public LinkException() { + } public LinkException(String message) { super(message); diff --git a/bot/src/main/java/edu/java/bot/exception/link/LinkRegistrationException.java b/dto/src/main/java/edu/java/dto/utils/exception/LinkRegistrationException.java similarity index 63% rename from bot/src/main/java/edu/java/bot/exception/link/LinkRegistrationException.java rename to dto/src/main/java/edu/java/dto/utils/exception/LinkRegistrationException.java index 2c3b5ff..ce9eca7 100644 --- a/bot/src/main/java/edu/java/bot/exception/link/LinkRegistrationException.java +++ b/dto/src/main/java/edu/java/dto/utils/exception/LinkRegistrationException.java @@ -1,6 +1,9 @@ -package edu.java.bot.exception.link; +package edu.java.dto.utils.exception; public class LinkRegistrationException extends LinkException { + public LinkRegistrationException() { + } + public LinkRegistrationException(String message) { super(message); } diff --git a/bot/src/main/java/edu/java/bot/exception/link/NotLinkException.java b/dto/src/main/java/edu/java/dto/utils/exception/NotLinkException.java similarity index 63% rename from bot/src/main/java/edu/java/bot/exception/link/NotLinkException.java rename to dto/src/main/java/edu/java/dto/utils/exception/NotLinkException.java index b3124f9..8205cc3 100644 --- a/bot/src/main/java/edu/java/bot/exception/link/NotLinkException.java +++ b/dto/src/main/java/edu/java/dto/utils/exception/NotLinkException.java @@ -1,6 +1,9 @@ -package edu.java.bot.exception.link; +package edu.java.dto.utils.exception; public class NotLinkException extends LinkException { + public NotLinkException() { + } + public NotLinkException(String message) { super(message); } diff --git a/dto/src/main/java/edu/java/dto/utils/exception/SourceException.java b/dto/src/main/java/edu/java/dto/utils/exception/SourceException.java new file mode 100644 index 0000000..6a1b06f --- /dev/null +++ b/dto/src/main/java/edu/java/dto/utils/exception/SourceException.java @@ -0,0 +1,10 @@ +package edu.java.dto.utils.exception; + +public abstract class SourceException extends LinkException { + public SourceException() { + } + + public SourceException(String message) { + super(message); + } +} diff --git a/dto/src/main/java/edu/java/dto/utils/exception/SourceNotSupportedException.java b/dto/src/main/java/edu/java/dto/utils/exception/SourceNotSupportedException.java new file mode 100644 index 0000000..7dc2801 --- /dev/null +++ b/dto/src/main/java/edu/java/dto/utils/exception/SourceNotSupportedException.java @@ -0,0 +1,10 @@ +package edu.java.dto.utils.exception; + +public class SourceNotSupportedException extends SourceException { + public SourceNotSupportedException() { + } + + public SourceNotSupportedException(String message) { + super(message); + } +} diff --git a/dto/src/main/java/edu/java/dto/utils/sources/info/GithubInfo.java b/dto/src/main/java/edu/java/dto/utils/sources/info/GithubInfo.java new file mode 100644 index 0000000..9262dfb --- /dev/null +++ b/dto/src/main/java/edu/java/dto/utils/sources/info/GithubInfo.java @@ -0,0 +1,19 @@ +package edu.java.dto.utils.sources.info; + +public class GithubInfo extends SourceInfo { + private final String owner; + private final String repo; + + public GithubInfo(String owner, String repo) { + this.owner = owner; + this.repo = repo; + } + + public String getOwner() { + return owner; + } + + public String getRepo() { + return repo; + } +} diff --git a/dto/src/main/java/edu/java/dto/utils/sources/info/SourceInfo.java b/dto/src/main/java/edu/java/dto/utils/sources/info/SourceInfo.java new file mode 100644 index 0000000..aeeff17 --- /dev/null +++ b/dto/src/main/java/edu/java/dto/utils/sources/info/SourceInfo.java @@ -0,0 +1,5 @@ +package edu.java.dto.utils.sources.info; + +public abstract class SourceInfo { + +} diff --git a/dto/src/main/java/edu/java/dto/utils/sources/info/StackoverflowInfo.java b/dto/src/main/java/edu/java/dto/utils/sources/info/StackoverflowInfo.java new file mode 100644 index 0000000..b9269f8 --- /dev/null +++ b/dto/src/main/java/edu/java/dto/utils/sources/info/StackoverflowInfo.java @@ -0,0 +1,13 @@ +package edu.java.dto.utils.sources.info; + +public class StackoverflowInfo extends SourceInfo { + private final long questionId; + + public StackoverflowInfo(long questionId) { + this.questionId = questionId; + } + + public long getQuestionId() { + return questionId; + } +} diff --git a/dto/src/main/java/edu/java/dto/utils/sources/parsers/GithubParser.java b/dto/src/main/java/edu/java/dto/utils/sources/parsers/GithubParser.java new file mode 100644 index 0000000..6e344c7 --- /dev/null +++ b/dto/src/main/java/edu/java/dto/utils/sources/parsers/GithubParser.java @@ -0,0 +1,31 @@ +package edu.java.dto.utils.sources.parsers; + +import edu.java.dto.utils.exception.BadSourceUrlException; +import edu.java.dto.utils.exception.SourceException; +import edu.java.dto.utils.exception.SourceNotSupportedException; +import edu.java.dto.utils.sources.info.GithubInfo; +import java.net.URI; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class GithubParser extends SourceParser { + private static final String BASE_PATTERN = "^https?://github.com"; + private static final String LINK_PATTERN = BASE_PATTERN + "/(.+?)/(.+?)(/.*)?$"; + + @Override + GithubInfo parseSource(URI uri) throws SourceException { + String link = uri.toString(); + Pattern pattern = Pattern.compile(LINK_PATTERN); + Matcher matcher = pattern.matcher(link); + if (matcher.find()) { + String owner = matcher.group(1); + String repo = matcher.group(2); + return new GithubInfo(owner, repo); + } + if (link.matches(BASE_PATTERN + ".*")) { + throw new BadSourceUrlException("Некорретная ссылка для GitHub, требуется формат\n" + + "\"http(-s)://github.com/{owner}/{repo}[/...]\""); + } + throw new SourceNotSupportedException(); + } +} diff --git a/dto/src/main/java/edu/java/dto/utils/sources/parsers/SourceParser.java b/dto/src/main/java/edu/java/dto/utils/sources/parsers/SourceParser.java new file mode 100644 index 0000000..9a72fcf --- /dev/null +++ b/dto/src/main/java/edu/java/dto/utils/sources/parsers/SourceParser.java @@ -0,0 +1,37 @@ +package edu.java.dto.utils.sources.parsers; + +import edu.java.dto.utils.exception.SourceException; +import edu.java.dto.utils.exception.SourceNotSupportedException; +import edu.java.dto.utils.sources.info.SourceInfo; +import java.net.URI; +import java.util.Set; + +public abstract class SourceParser { + protected SourceParser next; + + public void setNext(SourceParser next) { + this.next = next; + } + + public SourceInfo parse(URI uri) throws SourceException { + try { + return parseSource(uri); + } catch (SourceNotSupportedException e) { + if (next != null) { + return next.parse(uri); + } + throw e; + } + } + + abstract SourceInfo parseSource(URI uri) throws SourceException; + + public static SourceParser buildChain(Set parsers) { + SourceParser first = null; + for (SourceParser parser : parsers) { + parser.setNext(first); + first = parser; + } + return first; + } +} diff --git a/dto/src/main/java/edu/java/dto/utils/sources/parsers/StackoverflowParser.java b/dto/src/main/java/edu/java/dto/utils/sources/parsers/StackoverflowParser.java new file mode 100644 index 0000000..3cd5935 --- /dev/null +++ b/dto/src/main/java/edu/java/dto/utils/sources/parsers/StackoverflowParser.java @@ -0,0 +1,30 @@ +package edu.java.dto.utils.sources.parsers; + +import edu.java.dto.utils.exception.BadSourceUrlException; +import edu.java.dto.utils.exception.SourceException; +import edu.java.dto.utils.exception.SourceNotSupportedException; +import edu.java.dto.utils.sources.info.StackoverflowInfo; +import java.net.URI; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class StackoverflowParser extends SourceParser { + private static final String BASE_PATTERN = "^https?://stackoverflow.com/questions"; + private static final String LINK_PATTERN = BASE_PATTERN + "/([0-9]+?)(/.*)?$"; + + @Override + StackoverflowInfo parseSource(URI uri) throws SourceException { + String link = uri.toString(); + Pattern pattern = Pattern.compile(LINK_PATTERN); + Matcher matcher = pattern.matcher(link); + if (matcher.find()) { + long questionId = Long.parseLong(matcher.group(1)); + return new StackoverflowInfo(questionId); + } + if (link.matches(BASE_PATTERN + ".*")) { + throw new BadSourceUrlException("Некорретная ссылка для Stackoverflow, требуется формат\n" + + "\"http(-s)://stackoverflow.com/{questionId}[/...]\""); + } + throw new SourceNotSupportedException(); + } +} diff --git a/pom.xml b/pom.xml index e813daf..02e04cf 100644 --- a/pom.xml +++ b/pom.xml @@ -49,6 +49,7 @@ bot scrapper + dto diff --git a/scrapper/pom.xml b/scrapper/pom.xml index 3370247..b8833da 100644 --- a/scrapper/pom.xml +++ b/scrapper/pom.xml @@ -138,6 +138,12 @@ kafka test + + edu.java + dto + 0.1 + compile + diff --git a/scrapper/src/main/java/edu/java/api/client/BotClient.java b/scrapper/src/main/java/edu/java/api/client/BotClient.java deleted file mode 100644 index 023466f..0000000 --- a/scrapper/src/main/java/edu/java/api/client/BotClient.java +++ /dev/null @@ -1,12 +0,0 @@ -package edu.java.api.client; - -import com.fasterxml.jackson.databind.ObjectMapper; -import edu.java.client.AbstractClient; -import lombok.NonNull; - -public class BotClient extends AbstractClient { - - public BotClient(@NonNull String baseUrl, ObjectMapper mapper) { - super(baseUrl, mapper); - } -} diff --git a/scrapper/src/main/java/edu/java/api/exception/chat/ChatRegistrationException.java b/scrapper/src/main/java/edu/java/api/exception/chat/ChatRegistrationException.java deleted file mode 100644 index d804ca0..0000000 --- a/scrapper/src/main/java/edu/java/api/exception/chat/ChatRegistrationException.java +++ /dev/null @@ -1,4 +0,0 @@ -package edu.java.api.exception.chat; - -public class ChatRegistrationException extends Exception { -} diff --git a/scrapper/src/main/java/edu/java/ScrapperApplication.java b/scrapper/src/main/java/edu/java/scrapper/ScrapperApplication.java similarity index 85% rename from scrapper/src/main/java/edu/java/ScrapperApplication.java rename to scrapper/src/main/java/edu/java/scrapper/ScrapperApplication.java index fadceb1..22cb3aa 100644 --- a/scrapper/src/main/java/edu/java/ScrapperApplication.java +++ b/scrapper/src/main/java/edu/java/scrapper/ScrapperApplication.java @@ -1,6 +1,6 @@ -package edu.java; +package edu.java.scrapper; -import edu.java.configuration.ApplicationConfig; +import edu.java.scrapper.configuration.ApplicationConfig; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.context.properties.EnableConfigurationProperties; diff --git a/scrapper/src/main/java/edu/java/api/controller/ChatController.java b/scrapper/src/main/java/edu/java/scrapper/api/controller/ChatController.java similarity index 79% rename from scrapper/src/main/java/edu/java/api/controller/ChatController.java rename to scrapper/src/main/java/edu/java/scrapper/api/controller/ChatController.java index 92849c1..25fff0f 100644 --- a/scrapper/src/main/java/edu/java/api/controller/ChatController.java +++ b/scrapper/src/main/java/edu/java/scrapper/api/controller/ChatController.java @@ -1,6 +1,9 @@ -package edu.java.api.controller; +package edu.java.scrapper.api.controller; -import edu.java.api.dto.ApiErrorResponse; +import edu.java.dto.api.scrapper.ApiErrorResponse; +import edu.java.scrapper.api.exception.chat.ChatAlreadyRegisteredException; +import edu.java.scrapper.api.exception.chat.ChatNotFoundException; +import edu.java.scrapper.api.service.ChatService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.media.Content; import io.swagger.v3.oas.annotations.media.Schema; @@ -17,6 +20,11 @@ @RestController @RequestMapping("/tg-chat") public class ChatController { + private final ChatService service; + + public ChatController(ChatService service) { + this.service = service; + } @Operation(summary = "Зарегистрировать чат") @ApiResponses(value = { @@ -26,11 +34,8 @@ public class ChatController { schema = @Schema(implementation = ApiErrorResponse.class))}) }) @PostMapping("/{id}") - public ResponseEntity registerChat(@PathVariable @Positive long id) { - /* - TODO: регистрация чата - может быть брошено ChatRegistrationException - */ + public ResponseEntity registerChat(@PathVariable @Positive long id) throws ChatAlreadyRegisteredException { + service.register(id); return ResponseEntity.ok().build(); } @@ -45,11 +50,8 @@ public ResponseEntity registerChat(@PathVariable @Positive long id) { schema = @Schema(implementation = ApiErrorResponse.class))}) }) @DeleteMapping("/{id}") - public ResponseEntity deleteChat(@PathVariable @Positive long id) { - /* - TODO: удаление чата - может быть брошено ChatNotFoundException - */ + public ResponseEntity deleteChat(@PathVariable @Positive long id) throws ChatNotFoundException { + service.unregister(id); return ResponseEntity.ok().build(); } } diff --git a/scrapper/src/main/java/edu/java/api/controller/LinkController.java b/scrapper/src/main/java/edu/java/scrapper/api/controller/LinkController.java similarity index 70% rename from scrapper/src/main/java/edu/java/api/controller/LinkController.java rename to scrapper/src/main/java/edu/java/scrapper/api/controller/LinkController.java index 5829aed..6f40192 100644 --- a/scrapper/src/main/java/edu/java/api/controller/LinkController.java +++ b/scrapper/src/main/java/edu/java/scrapper/api/controller/LinkController.java @@ -1,16 +1,23 @@ -package edu.java.api.controller; +package edu.java.scrapper.api.controller; -import edu.java.api.dto.AddLinkRequest; -import edu.java.api.dto.ApiErrorResponse; -import edu.java.api.dto.LinkResponse; -import edu.java.api.dto.ListLinksResponse; -import edu.java.api.dto.RemoveLinkRequest; +import edu.java.dto.api.scrapper.AddLinkRequest; +import edu.java.dto.api.scrapper.ApiErrorResponse; +import edu.java.dto.api.scrapper.LinkResponse; +import edu.java.dto.api.scrapper.ListLinksResponse; +import edu.java.dto.api.scrapper.RemoveLinkRequest; +import edu.java.scrapper.api.domain.dto.Link; +import edu.java.scrapper.api.exception.chat.ChatNotFoundException; +import edu.java.scrapper.api.exception.link.LinkAdditionException; +import edu.java.scrapper.api.exception.link.LinkNotFoundException; +import edu.java.scrapper.api.service.LinkService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.media.Content; import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.responses.ApiResponses; import jakarta.validation.Valid; +import java.net.URI; +import java.util.List; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; @@ -23,6 +30,11 @@ @RestController @RequestMapping("/links") public class LinkController { + private final LinkService service; + + public LinkController(LinkService service) { + this.service = service; + } @Operation(summary = "Получить все отслеживаемые ссылки") @ApiResponses(value = { @@ -34,11 +46,15 @@ public class LinkController { schema = @Schema(implementation = ApiErrorResponse.class))}) }) @GetMapping - public ResponseEntity getLinks(@RequestHeader(name = "Tg-Chat-Id") long chatId) { - /* - TODO: получение списка ссылок - */ - return ResponseEntity.ok(new ListLinksResponse(new LinkResponse[0], 1)); + public ResponseEntity getLinks(@RequestHeader(name = "Tg-Chat-Id") long chatId) + throws ChatNotFoundException { + List links = service.listAll(chatId); + return ResponseEntity.ok(new ListLinksResponse( + links.stream() + .map(l -> new LinkResponse(l.id(), l.url())) + .toArray(LinkResponse[]::new), + links.size() + )); } @Operation(summary = "Добавить отслеживание ссылки") @@ -54,12 +70,9 @@ public ResponseEntity getLinks(@RequestHeader(name = "Tg-Chat public ResponseEntity addLink( @RequestHeader(name = "Tg-Chat-Id") long chatId, @Valid @RequestBody AddLinkRequest request - ) { - /* - TODO: добавление ссылки - может быть брошено LinkAdditionException - */ - return ResponseEntity.ok(new LinkResponse(0, "")); + ) throws LinkAdditionException, ChatNotFoundException { + Link link = service.add(chatId, URI.create(request.link())); + return ResponseEntity.ok(new LinkResponse(link.id(), link.url())); } @Operation(summary = "Убрать отслеживание ссылки") @@ -78,11 +91,8 @@ public ResponseEntity addLink( public ResponseEntity deleteLink( @RequestHeader(name = "Tg-Chat-Id") long chatId, @Valid @RequestBody RemoveLinkRequest request - ) { - /* - TODO: удаление ссылки - может быть брошено LinkNotFoundException - */ - return ResponseEntity.ok(new LinkResponse(0, "")); + ) throws ChatNotFoundException, LinkNotFoundException { + Link link = service.remove(chatId, URI.create(request.link())); + return ResponseEntity.ok(new LinkResponse(link.id(), link.url())); } } diff --git a/scrapper/src/main/java/edu/java/scrapper/api/domain/dto/Chat.java b/scrapper/src/main/java/edu/java/scrapper/api/domain/dto/Chat.java new file mode 100644 index 0000000..92db131 --- /dev/null +++ b/scrapper/src/main/java/edu/java/scrapper/api/domain/dto/Chat.java @@ -0,0 +1,4 @@ +package edu.java.scrapper.api.domain.dto; + +public record Chat(Long id, long tgId) { +} diff --git a/scrapper/src/main/java/edu/java/scrapper/api/domain/dto/Link.java b/scrapper/src/main/java/edu/java/scrapper/api/domain/dto/Link.java new file mode 100644 index 0000000..f96249d --- /dev/null +++ b/scrapper/src/main/java/edu/java/scrapper/api/domain/dto/Link.java @@ -0,0 +1,7 @@ +package edu.java.scrapper.api.domain.dto; + +import java.net.URI; +import java.time.OffsetDateTime; + +public record Link(Long id, URI url, OffsetDateTime lastUpdate, OffsetDateTime lastCheck) { +} diff --git a/scrapper/src/main/java/edu/java/scrapper/api/domain/dto/Subscription.java b/scrapper/src/main/java/edu/java/scrapper/api/domain/dto/Subscription.java new file mode 100644 index 0000000..8ce08d3 --- /dev/null +++ b/scrapper/src/main/java/edu/java/scrapper/api/domain/dto/Subscription.java @@ -0,0 +1,4 @@ +package edu.java.scrapper.api.domain.dto; + +public record Subscription(long chatId, long linkId) { +} diff --git a/scrapper/src/main/java/edu/java/scrapper/api/domain/repository/ChatRepository.java b/scrapper/src/main/java/edu/java/scrapper/api/domain/repository/ChatRepository.java new file mode 100644 index 0000000..61c36ff --- /dev/null +++ b/scrapper/src/main/java/edu/java/scrapper/api/domain/repository/ChatRepository.java @@ -0,0 +1,15 @@ +package edu.java.scrapper.api.domain.repository; + +import edu.java.scrapper.api.domain.dto.Chat; +import java.util.List; +import java.util.Optional; + +public interface ChatRepository { + Optional findByTgId(long tgId); + + Chat add(long tgId); + + void remove(long tgId); + + List findAll(); +} diff --git a/scrapper/src/main/java/edu/java/scrapper/api/domain/repository/LinkRepository.java b/scrapper/src/main/java/edu/java/scrapper/api/domain/repository/LinkRepository.java new file mode 100644 index 0000000..d9a323f --- /dev/null +++ b/scrapper/src/main/java/edu/java/scrapper/api/domain/repository/LinkRepository.java @@ -0,0 +1,25 @@ +package edu.java.scrapper.api.domain.repository; + +import edu.java.scrapper.api.domain.dto.Link; +import java.net.URI; +import java.time.OffsetDateTime; +import java.util.List; +import java.util.Optional; + +public interface LinkRepository { + Optional findById(long id); + + Optional findByUrl(URI url); + + Link add(Link link); + + void remove(URI url); + + void remove(long id); + + List findAll(); + + List findAllWithLastCheckOlderThan(OffsetDateTime time); + + void update(Link link); +} diff --git a/scrapper/src/main/java/edu/java/scrapper/api/domain/repository/SubscriptionRepository.java b/scrapper/src/main/java/edu/java/scrapper/api/domain/repository/SubscriptionRepository.java new file mode 100644 index 0000000..63c98d9 --- /dev/null +++ b/scrapper/src/main/java/edu/java/scrapper/api/domain/repository/SubscriptionRepository.java @@ -0,0 +1,28 @@ +package edu.java.scrapper.api.domain.repository; + +import edu.java.scrapper.api.domain.dto.Chat; +import edu.java.scrapper.api.domain.dto.Link; +import edu.java.scrapper.api.domain.dto.Subscription; +import java.net.URI; +import java.util.List; +import java.util.Optional; + +public interface SubscriptionRepository { + Optional find(long tgId, URI url); + + Subscription add(Subscription subscription); + + void remove(Subscription subscription); + + List findAll(); + + List findAllByChat(Chat chat); + + List findAllByLink(Link link); + + boolean linkNotFollowedByAnyone(long linkId); + + List findAllLinksByChat(Chat chat); + + List findAllChatsByLink(Link link); +} diff --git a/scrapper/src/main/java/edu/java/scrapper/api/domain/repository/jdbc/AbstractJdbcRepository.java b/scrapper/src/main/java/edu/java/scrapper/api/domain/repository/jdbc/AbstractJdbcRepository.java new file mode 100644 index 0000000..2820fe6 --- /dev/null +++ b/scrapper/src/main/java/edu/java/scrapper/api/domain/repository/jdbc/AbstractJdbcRepository.java @@ -0,0 +1,11 @@ +package edu.java.scrapper.api.domain.repository.jdbc; + +import org.springframework.jdbc.core.simple.JdbcClient; + +public abstract class AbstractJdbcRepository { + protected final JdbcClient client; + + public AbstractJdbcRepository(JdbcClient client) { + this.client = client; + } +} diff --git a/scrapper/src/main/java/edu/java/scrapper/api/domain/repository/jdbc/JdbcChatRepository.java b/scrapper/src/main/java/edu/java/scrapper/api/domain/repository/jdbc/JdbcChatRepository.java new file mode 100644 index 0000000..d1a1000 --- /dev/null +++ b/scrapper/src/main/java/edu/java/scrapper/api/domain/repository/jdbc/JdbcChatRepository.java @@ -0,0 +1,51 @@ +package edu.java.scrapper.api.domain.repository.jdbc; + +import edu.java.scrapper.api.domain.dto.Chat; +import edu.java.scrapper.api.domain.repository.ChatRepository; +import java.util.List; +import java.util.Optional; +import org.springframework.jdbc.core.simple.JdbcClient; +import org.springframework.jdbc.support.GeneratedKeyHolder; +import org.springframework.stereotype.Repository; +import org.springframework.transaction.annotation.Transactional; + +@Repository +@Transactional +public class JdbcChatRepository extends AbstractJdbcRepository implements ChatRepository { + + public JdbcChatRepository(JdbcClient client) { + super(client); + } + + @Override + public Optional findByTgId(long tgId) { + return client.sql("select * from chat where tg_id = ?") + .param(tgId) + .query(Chat.class) + .optional(); + } + + @Override + public Chat add(long tgId) { + GeneratedKeyHolder keyHolder = new GeneratedKeyHolder(); + client.sql("insert into chat (tg_id) values (?)") + .param(tgId) + .update(keyHolder, "id"); + long id = keyHolder.getKey().longValue(); + return new Chat(id, tgId); + } + + @Override + public void remove(long tgId) { + client.sql("delete from chat where tg_id = ?") + .param(tgId) + .update(); + } + + @Override + public List findAll() { + return client.sql("select * from chat") + .query(Chat.class) + .list(); + } +} diff --git a/scrapper/src/main/java/edu/java/scrapper/api/domain/repository/jdbc/JdbcLinkRepository.java b/scrapper/src/main/java/edu/java/scrapper/api/domain/repository/jdbc/JdbcLinkRepository.java new file mode 100644 index 0000000..7601691 --- /dev/null +++ b/scrapper/src/main/java/edu/java/scrapper/api/domain/repository/jdbc/JdbcLinkRepository.java @@ -0,0 +1,87 @@ +package edu.java.scrapper.api.domain.repository.jdbc; + +import edu.java.scrapper.api.domain.dto.Link; +import edu.java.scrapper.api.domain.repository.LinkRepository; +import java.net.URI; +import java.time.OffsetDateTime; +import java.util.List; +import java.util.Optional; +import org.springframework.jdbc.core.simple.JdbcClient; +import org.springframework.jdbc.support.GeneratedKeyHolder; +import org.springframework.stereotype.Repository; +import org.springframework.transaction.annotation.Transactional; + +@Repository +@Transactional +public class JdbcLinkRepository extends AbstractJdbcRepository implements LinkRepository { + + public JdbcLinkRepository(JdbcClient client) { + super(client); + } + + @Override + public Optional findById(long id) { + return client.sql("select * from link where id = ?") + .param(id) + .query(Link.class) + .optional(); + } + + @Override + public Optional findByUrl(URI url) { + return client.sql("select * from link where url = ?") + .param(url.toString()) + .query(Link.class) + .optional(); + } + + @Override + public Link add(Link link) { + GeneratedKeyHolder keyHolder = new GeneratedKeyHolder(); + client.sql("insert into link (url, last_update, last_check) values (?, ?, ?)") + .param(link.url().toString()) + .param(link.lastUpdate()) + .param(link.lastCheck()) + .update(keyHolder, "id"); + long id = keyHolder.getKey().longValue(); + return new Link(id, link.url(), link.lastUpdate(), link.lastCheck()); + } + + @Override + public void remove(URI url) { + client.sql("delete from link where url = ?") + .param(url.toString()) + .update(); + } + + @Override + public void remove(long id) { + client.sql("delete from link where id = ?") + .param(id) + .update(); + } + + @Override + public List findAll() { + return client.sql("select * from link") + .query(Link.class) + .list(); + } + + @Override + public List findAllWithLastCheckOlderThan(OffsetDateTime time) { + return client.sql("select * from link where link.last_check < ?") + .param(time) + .query(Link.class) + .list(); + } + + @Override + public void update(Link link) { + client.sql("update link set last_update = ?, last_check = ? where id = ?") + .param(link.lastUpdate()) + .param(link.lastCheck()) + .param(link.id()) + .update(); + } +} diff --git a/scrapper/src/main/java/edu/java/scrapper/api/domain/repository/jdbc/JdbcSubscriptionRepository.java b/scrapper/src/main/java/edu/java/scrapper/api/domain/repository/jdbc/JdbcSubscriptionRepository.java new file mode 100644 index 0000000..91b9f27 --- /dev/null +++ b/scrapper/src/main/java/edu/java/scrapper/api/domain/repository/jdbc/JdbcSubscriptionRepository.java @@ -0,0 +1,114 @@ +package edu.java.scrapper.api.domain.repository.jdbc; + +import edu.java.scrapper.api.domain.dto.Chat; +import edu.java.scrapper.api.domain.dto.Link; +import edu.java.scrapper.api.domain.dto.Subscription; +import edu.java.scrapper.api.domain.repository.SubscriptionRepository; +import java.net.URI; +import java.util.List; +import java.util.Optional; +import org.springframework.jdbc.core.simple.JdbcClient; +import org.springframework.stereotype.Repository; +import org.springframework.transaction.annotation.Transactional; + +@Repository +@Transactional +public class JdbcSubscriptionRepository extends AbstractJdbcRepository implements SubscriptionRepository { + + public JdbcSubscriptionRepository(JdbcClient client) { + super(client); + } + + @Override + public Optional find(long tgId, URI url) { + return client.sql(""" + select subscription.* + from subscription + inner join chat on subscription.chat_id = chat.id + inner join link on subscription.link_id = link.id + where + chat.tg_id = ? + and link.url = ?""") + .param(tgId) + .param(url.toString()) + .query(Subscription.class) + .optional(); + } + + @Override + public Subscription add(Subscription subscription) { + client.sql("insert into subscription (chat_id, link_id) values (?, ?)") + .param(subscription.chatId()) + .param(subscription.linkId()) + .update(); + return subscription; + } + + @Override + public void remove(Subscription subscription) { + client.sql("delete from subscription where chat_id = ? and link_id = ?") + .param(subscription.chatId()) + .param(subscription.linkId()) + .update(); + } + + @Override + public List findAll() { + return client.sql("select * from subscription") + .query(Subscription.class) + .list(); + } + + @Override + public List findAllByChat(Chat chat) { + return client.sql("select * from subscription where chat_id = ?") + .param(chat.id()) + .query(Subscription.class) + .list(); + } + + @SuppressWarnings("MultipleStringLiterals") + @Override + public List findAllByLink(Link link) { + return client.sql("select * from subscription where link_id = ?") + .param(link.id()) + .query(Subscription.class) + .list(); + } + + @SuppressWarnings("MultipleStringLiterals") + @Override + public boolean linkNotFollowedByAnyone(long linkId) { + List subscriptions = client.sql("select * from subscription where link_id = ?") + .param(linkId) + .query(Subscription.class) + .list(); + return subscriptions.isEmpty(); + } + + @Override + public List findAllLinksByChat(Chat chat) { + return client.sql(""" + select link.* + from link + inner join subscription on link.id = subscription.link_id + where + subscription.chat_id = ?""") + .param(chat.id()) + .query(Link.class) + .list(); + } + + @Override + public List findAllChatsByLink(Link link) { + return client.sql(""" + select chat.* + from chat + inner join subscription on chat.id = subscription.chat_id + where + subscription.link_id = ?""") + .param(link.id()) + .query(Chat.class) + .list(); + } +} diff --git a/scrapper/src/main/java/edu/java/scrapper/api/exception/chat/ChatAlreadyRegisteredException.java b/scrapper/src/main/java/edu/java/scrapper/api/exception/chat/ChatAlreadyRegisteredException.java new file mode 100644 index 0000000..f0333fc --- /dev/null +++ b/scrapper/src/main/java/edu/java/scrapper/api/exception/chat/ChatAlreadyRegisteredException.java @@ -0,0 +1,4 @@ +package edu.java.scrapper.api.exception.chat; + +public class ChatAlreadyRegisteredException extends Exception { +} diff --git a/scrapper/src/main/java/edu/java/api/exception/chat/ChatExceptionApiHandler.java b/scrapper/src/main/java/edu/java/scrapper/api/exception/chat/ChatExceptionApiHandler.java similarity index 59% rename from scrapper/src/main/java/edu/java/api/exception/chat/ChatExceptionApiHandler.java rename to scrapper/src/main/java/edu/java/scrapper/api/exception/chat/ChatExceptionApiHandler.java index 4483f07..3a14e19 100644 --- a/scrapper/src/main/java/edu/java/api/exception/chat/ChatExceptionApiHandler.java +++ b/scrapper/src/main/java/edu/java/scrapper/api/exception/chat/ChatExceptionApiHandler.java @@ -1,6 +1,7 @@ -package edu.java.api.exception.chat; +package edu.java.scrapper.api.exception.chat; -import edu.java.api.dto.ApiErrorResponse; +import edu.java.dto.api.scrapper.ApiErrorResponse; +import java.util.Arrays; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ExceptionHandler; @@ -9,14 +10,16 @@ @RestControllerAdvice public class ChatExceptionApiHandler { - @ExceptionHandler(ChatRegistrationException.class) - public ResponseEntity chatRegistrationException(ChatRegistrationException exception) { + @ExceptionHandler(ChatAlreadyRegisteredException.class) + public ResponseEntity chatRegistrationException(ChatAlreadyRegisteredException exception) { ApiErrorResponse error = new ApiErrorResponse( - "Чат с уже зарегистрирован", + "Чат уже зарегистрирован", HttpStatus.BAD_REQUEST.toString(), exception.getClass().getName(), exception.getMessage(), - exception.getStackTrace() + Arrays.stream(exception.getStackTrace()) + .map(StackTraceElement::toString) + .toArray(String[]::new) ); return ResponseEntity .status(HttpStatus.BAD_REQUEST) @@ -26,11 +29,13 @@ public ResponseEntity chatRegistrationException(ChatRegistrati @ExceptionHandler(ChatNotFoundException.class) public ResponseEntity chatNotFoundException(ChatNotFoundException exception) { ApiErrorResponse error = new ApiErrorResponse( - "Чат с id не найден", + "Чат не найден", HttpStatus.NOT_FOUND.toString(), exception.getClass().getName(), exception.getMessage(), - exception.getStackTrace() + Arrays.stream(exception.getStackTrace()) + .map(StackTraceElement::toString) + .toArray(String[]::new) ); return ResponseEntity .status(HttpStatus.NOT_FOUND) @@ -40,14 +45,16 @@ public ResponseEntity chatNotFoundException(ChatNotFoundExcept @ExceptionHandler(Exception.class) public ResponseEntity anyException(Exception exception) { ApiErrorResponse error = new ApiErrorResponse( - "Неверные параметры запроса", - HttpStatus.BAD_REQUEST.toString(), + "Произошла непредвиденная ошибка на стороне сервера", + HttpStatus.INTERNAL_SERVER_ERROR.toString(), exception.getClass().getName(), exception.getMessage(), - exception.getStackTrace() + Arrays.stream(exception.getStackTrace()) + .map(StackTraceElement::toString) + .toArray(String[]::new) ); return ResponseEntity - .status(HttpStatus.BAD_REQUEST) + .status(HttpStatus.INTERNAL_SERVER_ERROR) .body(error); } } diff --git a/scrapper/src/main/java/edu/java/api/exception/chat/ChatNotFoundException.java b/scrapper/src/main/java/edu/java/scrapper/api/exception/chat/ChatNotFoundException.java similarity index 55% rename from scrapper/src/main/java/edu/java/api/exception/chat/ChatNotFoundException.java rename to scrapper/src/main/java/edu/java/scrapper/api/exception/chat/ChatNotFoundException.java index 35cf0c7..647066d 100644 --- a/scrapper/src/main/java/edu/java/api/exception/chat/ChatNotFoundException.java +++ b/scrapper/src/main/java/edu/java/scrapper/api/exception/chat/ChatNotFoundException.java @@ -1,4 +1,4 @@ -package edu.java.api.exception.chat; +package edu.java.scrapper.api.exception.chat; public class ChatNotFoundException extends Exception { } diff --git a/scrapper/src/main/java/edu/java/api/exception/link/LinkAdditionException.java b/scrapper/src/main/java/edu/java/scrapper/api/exception/link/LinkAdditionException.java similarity index 55% rename from scrapper/src/main/java/edu/java/api/exception/link/LinkAdditionException.java rename to scrapper/src/main/java/edu/java/scrapper/api/exception/link/LinkAdditionException.java index f5c22de..dbe67a6 100644 --- a/scrapper/src/main/java/edu/java/api/exception/link/LinkAdditionException.java +++ b/scrapper/src/main/java/edu/java/scrapper/api/exception/link/LinkAdditionException.java @@ -1,4 +1,4 @@ -package edu.java.api.exception.link; +package edu.java.scrapper.api.exception.link; public class LinkAdditionException extends Exception { } diff --git a/scrapper/src/main/java/edu/java/api/exception/link/LinkExceptionApiHandler.java b/scrapper/src/main/java/edu/java/scrapper/api/exception/link/LinkExceptionApiHandler.java similarity index 67% rename from scrapper/src/main/java/edu/java/api/exception/link/LinkExceptionApiHandler.java rename to scrapper/src/main/java/edu/java/scrapper/api/exception/link/LinkExceptionApiHandler.java index 7be1029..d17e833 100644 --- a/scrapper/src/main/java/edu/java/api/exception/link/LinkExceptionApiHandler.java +++ b/scrapper/src/main/java/edu/java/scrapper/api/exception/link/LinkExceptionApiHandler.java @@ -1,6 +1,7 @@ -package edu.java.api.exception.link; +package edu.java.scrapper.api.exception.link; -import edu.java.api.dto.ApiErrorResponse; +import edu.java.dto.api.scrapper.ApiErrorResponse; +import java.util.Arrays; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ExceptionHandler; @@ -16,7 +17,9 @@ public ResponseEntity linkAdditionException(LinkAdditionExcept HttpStatus.BAD_REQUEST.toString(), exception.getClass().getName(), exception.getMessage(), - exception.getStackTrace() + Arrays.stream(exception.getStackTrace()) + .map(StackTraceElement::toString) + .toArray(String[]::new) ); return ResponseEntity .status(HttpStatus.BAD_REQUEST) @@ -30,7 +33,9 @@ public ResponseEntity linkNotFoundException(LinkNotFoundExcept HttpStatus.NOT_FOUND.toString(), exception.getClass().getName(), exception.getMessage(), - exception.getStackTrace() + Arrays.stream(exception.getStackTrace()) + .map(StackTraceElement::toString) + .toArray(String[]::new) ); return ResponseEntity .status(HttpStatus.NOT_FOUND) @@ -40,14 +45,16 @@ public ResponseEntity linkNotFoundException(LinkNotFoundExcept @ExceptionHandler(Exception.class) public ResponseEntity anyException(Exception exception) { ApiErrorResponse error = new ApiErrorResponse( - "Неверные параметры запроса", - HttpStatus.BAD_REQUEST.toString(), + "Произошла непредвиденная ошибка на стороне сервера", + HttpStatus.INTERNAL_SERVER_ERROR.toString(), exception.getClass().getName(), exception.getMessage(), - exception.getStackTrace() + Arrays.stream(exception.getStackTrace()) + .map(StackTraceElement::toString) + .toArray(String[]::new) ); return ResponseEntity - .status(HttpStatus.BAD_REQUEST) + .status(HttpStatus.INTERNAL_SERVER_ERROR) .body(error); } } diff --git a/scrapper/src/main/java/edu/java/api/exception/link/LinkNotFoundException.java b/scrapper/src/main/java/edu/java/scrapper/api/exception/link/LinkNotFoundException.java similarity index 55% rename from scrapper/src/main/java/edu/java/api/exception/link/LinkNotFoundException.java rename to scrapper/src/main/java/edu/java/scrapper/api/exception/link/LinkNotFoundException.java index 879f43e..655dc36 100644 --- a/scrapper/src/main/java/edu/java/api/exception/link/LinkNotFoundException.java +++ b/scrapper/src/main/java/edu/java/scrapper/api/exception/link/LinkNotFoundException.java @@ -1,4 +1,4 @@ -package edu.java.api.exception.link; +package edu.java.scrapper.api.exception.link; public class LinkNotFoundException extends Exception { } diff --git a/scrapper/src/main/java/edu/java/scrapper/api/service/ChatService.java b/scrapper/src/main/java/edu/java/scrapper/api/service/ChatService.java new file mode 100644 index 0000000..1eb5ee4 --- /dev/null +++ b/scrapper/src/main/java/edu/java/scrapper/api/service/ChatService.java @@ -0,0 +1,10 @@ +package edu.java.scrapper.api.service; + +import edu.java.scrapper.api.exception.chat.ChatAlreadyRegisteredException; +import edu.java.scrapper.api.exception.chat.ChatNotFoundException; + +public interface ChatService { + void register(long tgId) throws ChatAlreadyRegisteredException; + + void unregister(long tgId) throws ChatNotFoundException; +} diff --git a/scrapper/src/main/java/edu/java/scrapper/api/service/LinkService.java b/scrapper/src/main/java/edu/java/scrapper/api/service/LinkService.java new file mode 100644 index 0000000..fd58f07 --- /dev/null +++ b/scrapper/src/main/java/edu/java/scrapper/api/service/LinkService.java @@ -0,0 +1,16 @@ +package edu.java.scrapper.api.service; + +import edu.java.scrapper.api.domain.dto.Link; +import edu.java.scrapper.api.exception.chat.ChatNotFoundException; +import edu.java.scrapper.api.exception.link.LinkAdditionException; +import edu.java.scrapper.api.exception.link.LinkNotFoundException; +import java.net.URI; +import java.util.List; + +public interface LinkService { + Link add(long tgId, URI url) throws ChatNotFoundException, LinkAdditionException; + + Link remove(long tgId, URI url) throws LinkNotFoundException, ChatNotFoundException; + + List listAll(long tgId) throws ChatNotFoundException; +} diff --git a/scrapper/src/main/java/edu/java/scrapper/api/service/LinkUpdater.java b/scrapper/src/main/java/edu/java/scrapper/api/service/LinkUpdater.java new file mode 100644 index 0000000..502bf89 --- /dev/null +++ b/scrapper/src/main/java/edu/java/scrapper/api/service/LinkUpdater.java @@ -0,0 +1,5 @@ +package edu.java.scrapper.api.service; + +public interface LinkUpdater { + int update(); +} diff --git a/scrapper/src/main/java/edu/java/scrapper/api/service/ScrapperService.java b/scrapper/src/main/java/edu/java/scrapper/api/service/ScrapperService.java new file mode 100644 index 0000000..3e79981 --- /dev/null +++ b/scrapper/src/main/java/edu/java/scrapper/api/service/ScrapperService.java @@ -0,0 +1,24 @@ +package edu.java.scrapper.api.service; + +import edu.java.scrapper.api.domain.repository.ChatRepository; +import edu.java.scrapper.api.domain.repository.LinkRepository; +import edu.java.scrapper.api.domain.repository.SubscriptionRepository; +import edu.java.scrapper.configuration.ApplicationConfig; + +public abstract class ScrapperService { + protected final ApplicationConfig config; + protected final ChatRepository chatRepo; + protected final LinkRepository linkRepo; + protected final SubscriptionRepository subscriptionRepo; + + protected ScrapperService( + ApplicationConfig config, ChatRepository chatRepo, + LinkRepository linkRepo, + SubscriptionRepository subscriptionRepo + ) { + this.config = config; + this.chatRepo = chatRepo; + this.linkRepo = linkRepo; + this.subscriptionRepo = subscriptionRepo; + } +} diff --git a/scrapper/src/main/java/edu/java/scrapper/api/service/jdbc/JdbcChatService.java b/scrapper/src/main/java/edu/java/scrapper/api/service/jdbc/JdbcChatService.java new file mode 100644 index 0000000..47a2b31 --- /dev/null +++ b/scrapper/src/main/java/edu/java/scrapper/api/service/jdbc/JdbcChatService.java @@ -0,0 +1,56 @@ +package edu.java.scrapper.api.service.jdbc; + +import edu.java.scrapper.api.domain.dto.Chat; +import edu.java.scrapper.api.domain.dto.Subscription; +import edu.java.scrapper.api.domain.repository.jdbc.JdbcChatRepository; +import edu.java.scrapper.api.domain.repository.jdbc.JdbcLinkRepository; +import edu.java.scrapper.api.domain.repository.jdbc.JdbcSubscriptionRepository; +import edu.java.scrapper.api.exception.chat.ChatAlreadyRegisteredException; +import edu.java.scrapper.api.exception.chat.ChatNotFoundException; +import edu.java.scrapper.api.service.ChatService; +import edu.java.scrapper.api.service.ScrapperService; +import edu.java.scrapper.configuration.ApplicationConfig; +import java.util.List; +import java.util.Optional; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Transactional +@Service +public class JdbcChatService extends ScrapperService implements ChatService { + + @Autowired + protected JdbcChatService( + ApplicationConfig config, + JdbcChatRepository chatRepo, JdbcLinkRepository linkRepo, + JdbcSubscriptionRepository subscriptionRepo + ) { + super(config, chatRepo, linkRepo, subscriptionRepo); + } + + @Override + public void register(long tgId) throws ChatAlreadyRegisteredException { + Optional chat = chatRepo.findByTgId(tgId); + if (chat.isPresent()) { + throw new ChatAlreadyRegisteredException(); + } + chatRepo.add(tgId); + } + + @Override + public void unregister(long tgId) throws ChatNotFoundException { + Optional chat = chatRepo.findByTgId(tgId); + if (chat.isEmpty()) { + throw new ChatNotFoundException(); + } + List subscriptions = subscriptionRepo.findAllByChat(chat.get()); + for (Subscription subscription : subscriptions) { + subscriptionRepo.remove(subscription); + if (subscriptionRepo.linkNotFollowedByAnyone(subscription.linkId())) { + linkRepo.remove(subscription.linkId()); + } + } + chatRepo.remove(tgId); + } +} diff --git a/scrapper/src/main/java/edu/java/scrapper/api/service/jdbc/JdbcLinkService.java b/scrapper/src/main/java/edu/java/scrapper/api/service/jdbc/JdbcLinkService.java new file mode 100644 index 0000000..ce6a854 --- /dev/null +++ b/scrapper/src/main/java/edu/java/scrapper/api/service/jdbc/JdbcLinkService.java @@ -0,0 +1,83 @@ +package edu.java.scrapper.api.service.jdbc; + +import edu.java.scrapper.api.domain.dto.Chat; +import edu.java.scrapper.api.domain.dto.Link; +import edu.java.scrapper.api.domain.dto.Subscription; +import edu.java.scrapper.api.domain.repository.jdbc.JdbcChatRepository; +import edu.java.scrapper.api.domain.repository.jdbc.JdbcLinkRepository; +import edu.java.scrapper.api.domain.repository.jdbc.JdbcSubscriptionRepository; +import edu.java.scrapper.api.exception.chat.ChatNotFoundException; +import edu.java.scrapper.api.exception.link.LinkAdditionException; +import edu.java.scrapper.api.exception.link.LinkNotFoundException; +import edu.java.scrapper.api.service.LinkService; +import edu.java.scrapper.api.service.ScrapperService; +import edu.java.scrapper.configuration.ApplicationConfig; +import java.net.URI; +import java.time.OffsetDateTime; +import java.time.ZoneOffset; +import java.time.temporal.ChronoUnit; +import java.util.List; +import java.util.Optional; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Transactional +@Service +public class JdbcLinkService extends ScrapperService implements LinkService { + + @Autowired + protected JdbcLinkService( + ApplicationConfig config, + JdbcChatRepository chatRepo, JdbcLinkRepository linkRepo, + JdbcSubscriptionRepository subscriptionRepo + ) { + super(config, chatRepo, linkRepo, subscriptionRepo); + } + + @Override + public Link add(long tgId, URI url) throws ChatNotFoundException, LinkAdditionException { + Optional subscription = subscriptionRepo.find(tgId, url); + if (subscription.isPresent()) { + throw new LinkAdditionException(); + } + Optional chat = chatRepo.findByTgId(tgId); + if (chat.isEmpty()) { + throw new ChatNotFoundException(); + } + Optional link = linkRepo.findByUrl(url); + if (link.isEmpty()) { + OffsetDateTime now = OffsetDateTime.now(ZoneOffset.UTC).truncatedTo(ChronoUnit.SECONDS); + link = Optional.of(linkRepo.add(new Link(null, url, now, now))); + } + subscriptionRepo.add(new Subscription(chat.get().id(), link.get().id())); + return link.get(); + } + + @Override + public Link remove(long tgId, URI url) throws LinkNotFoundException, ChatNotFoundException { + Optional chat = chatRepo.findByTgId(tgId); + if (chat.isEmpty()) { + throw new ChatNotFoundException(); + } + Optional subscription = subscriptionRepo.find(tgId, url); + if (subscription.isEmpty()) { + throw new LinkNotFoundException(); + } + Link link = linkRepo.findById(subscription.get().linkId()).get(); + subscriptionRepo.remove(subscription.get()); + if (subscriptionRepo.linkNotFollowedByAnyone(link.id())) { + linkRepo.remove(link.id()); + } + return link; + } + + @Override + public List listAll(long tgId) throws ChatNotFoundException { + Optional chat = chatRepo.findByTgId(tgId); + if (chat.isEmpty()) { + throw new ChatNotFoundException(); + } + return subscriptionRepo.findAllLinksByChat(chat.get()); + } +} diff --git a/scrapper/src/main/java/edu/java/scrapper/api/service/jdbc/JdbcLinkUpdater.java b/scrapper/src/main/java/edu/java/scrapper/api/service/jdbc/JdbcLinkUpdater.java new file mode 100644 index 0000000..7342648 --- /dev/null +++ b/scrapper/src/main/java/edu/java/scrapper/api/service/jdbc/JdbcLinkUpdater.java @@ -0,0 +1,109 @@ +package edu.java.scrapper.api.service.jdbc; + +import edu.java.dto.api.bot.LinkUpdateRequest; +import edu.java.dto.utils.LinkInfo; +import edu.java.dto.utils.exception.NotLinkException; +import edu.java.dto.utils.exception.SourceException; +import edu.java.scrapper.api.domain.dto.Chat; +import edu.java.scrapper.api.domain.dto.Link; +import edu.java.scrapper.api.domain.dto.Subscription; +import edu.java.scrapper.api.domain.repository.jdbc.JdbcChatRepository; +import edu.java.scrapper.api.domain.repository.jdbc.JdbcLinkRepository; +import edu.java.scrapper.api.domain.repository.jdbc.JdbcSubscriptionRepository; +import edu.java.scrapper.api.service.LinkUpdater; +import edu.java.scrapper.api.service.ScrapperService; +import edu.java.scrapper.client.sources.ResponseException; +import edu.java.scrapper.configuration.ApplicationConfig; +import edu.java.scrapper.configuration.ClientConfig; +import edu.java.scrapper.configuration.LinkParserConfig; +import edu.java.scrapper.shedule.update.dto.Update; +import edu.java.scrapper.shedule.update.sources.SourceUpdater; +import java.time.Duration; +import java.time.OffsetDateTime; +import java.time.ZoneOffset; +import java.time.temporal.ChronoUnit; +import java.util.List; +import java.util.Optional; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Transactional +@Service +public class JdbcLinkUpdater extends ScrapperService implements LinkUpdater { + private final ClientConfig clientConfig; + private final LinkParserConfig parserConfig; + + @Autowired + protected JdbcLinkUpdater( + ApplicationConfig config, LinkParserConfig parserConfig, + ClientConfig clientConfig, + JdbcChatRepository chatRepo, JdbcLinkRepository linkRepo, + JdbcSubscriptionRepository subscriptionRepo + ) { + super(config, chatRepo, linkRepo, subscriptionRepo); + this.parserConfig = parserConfig; + this.clientConfig = clientConfig; + } + + @Override + public int update() { + Duration checkDelay = config.scheduler().forceCheckDelay(); + OffsetDateTime lastCheck = OffsetDateTime.now(ZoneOffset.UTC).truncatedTo(ChronoUnit.SECONDS); + List linksNeedToCheck = linkRepo.findAllWithLastCheckOlderThan(lastCheck.minus(checkDelay)); + int countUpdates = 0; + for (Link link : linksNeedToCheck) { + linkRepo.update(new Link(link.id(), link.url(), link.lastUpdate(), lastCheck)); + try { + LinkInfo linkInfo = parserConfig.linkParser().parse(link.url().toString()); + Optional update = getUpdateFromSource(linkInfo); + if (update.isPresent() && update.get().getCreatedAt().isAfter(link.lastUpdate())) { + countUpdates++; + linkRepo.update(new Link(link.id(), link.url(), update.get().getCreatedAt(), lastCheck)); + processLinkUpdate(update.get(), link); + } + } catch (NotLinkException | SourceException | ResponseException e) { + removeCorruptedLinkWithSubscriptions(link); + } + } + return countUpdates; + } + + private void processLinkUpdate(Update update, Link link) throws SourceException, NotLinkException { + List subscribers = subscriptionRepo.findAllChatsByLink(link); + if (subscribers.isEmpty()) { + linkRepo.remove(link.url()); + return; + } + clientConfig.botClient().sendUpdate(new LinkUpdateRequest( + link.id(), + link.url(), + "Новое обновление по ссылке\n%s\nСоздано в %s" + .formatted(link.url(), update.getCreatedAt()), + subscribers.stream().map(Chat::tgId).toArray(Long[]::new) + )); + } + + private Optional getUpdateFromSource(LinkInfo linkInfo) throws ResponseException { + SourceUpdater updater = SourceUpdater.getUpdaterForSource(clientConfig, linkInfo.source()); + if (updater != null) { + return updater.getUpdate(linkInfo.source()); + } + return Optional.empty(); + } + + private void removeCorruptedLinkWithSubscriptions(Link link) { + List subscribers = subscriptionRepo.findAllChatsByLink(link); + for (Chat subscriber : subscribers) { + Subscription subscription = new Subscription(subscriber.id(), link.id()); + subscriptionRepo.remove(subscription); + } + linkRepo.remove(link.url()); + clientConfig.botClient().sendUpdate(new LinkUpdateRequest( + link.id(), + link.url(), + "Ссылка\n" + link.url() + "\n больше не доступна, поэтому она удалена из Ваших подписок", + subscribers.stream().map(Chat::tgId).toArray(Long[]::new) + )); + } +} diff --git a/bot/src/main/java/edu/java/bot/api/client/AbstractClient.java b/scrapper/src/main/java/edu/java/scrapper/client/AbstractClient.java similarity index 92% rename from bot/src/main/java/edu/java/bot/api/client/AbstractClient.java rename to scrapper/src/main/java/edu/java/scrapper/client/AbstractClient.java index 5ebef97..e73da2f 100644 --- a/bot/src/main/java/edu/java/bot/api/client/AbstractClient.java +++ b/scrapper/src/main/java/edu/java/scrapper/client/AbstractClient.java @@ -1,4 +1,4 @@ -package edu.java.bot.api.client; +package edu.java.scrapper.client; import com.fasterxml.jackson.databind.ObjectMapper; import lombok.NonNull; diff --git a/scrapper/src/main/java/edu/java/scrapper/client/bot/BotApiException.java b/scrapper/src/main/java/edu/java/scrapper/client/bot/BotApiException.java new file mode 100644 index 0000000..378c9be --- /dev/null +++ b/scrapper/src/main/java/edu/java/scrapper/client/bot/BotApiException.java @@ -0,0 +1,15 @@ +package edu.java.scrapper.client.bot; + +import edu.java.dto.api.bot.ApiErrorResponse; +import lombok.Getter; + +@Getter +public class BotApiException extends Exception { + private final ApiErrorResponse error; + + public BotApiException(ApiErrorResponse error) { + super("%s: %s. %s - %s\n%s".formatted(error.code(), error.description(), + error.exceptionName(), error.exceptionMessage(), String.join("\n", error.stacktrace()))); + this.error = error; + } +} diff --git a/scrapper/src/main/java/edu/java/scrapper/client/bot/BotClient.java b/scrapper/src/main/java/edu/java/scrapper/client/bot/BotClient.java new file mode 100644 index 0000000..6cba112 --- /dev/null +++ b/scrapper/src/main/java/edu/java/scrapper/client/bot/BotClient.java @@ -0,0 +1,35 @@ +package edu.java.scrapper.client.bot; + +import com.fasterxml.jackson.databind.ObjectMapper; +import edu.java.dto.api.bot.ApiErrorResponse; +import edu.java.dto.api.bot.LinkUpdateRequest; +import edu.java.scrapper.client.AbstractClient; +import lombok.NonNull; +import org.springframework.http.HttpStatusCode; +import org.springframework.http.MediaType; +import org.springframework.web.reactive.function.client.ClientResponse; +import reactor.core.publisher.Mono; + +public class BotClient extends AbstractClient { + + public BotClient(@NonNull String baseUrl, ObjectMapper mapper) { + super(baseUrl, mapper); + } + + public void sendUpdate(LinkUpdateRequest request) { + client.post() + .uri("/update") + .accept(MediaType.APPLICATION_JSON) + .body(Mono.just(request), LinkUpdateRequest.class) + .retrieve() + .onStatus(HttpStatusCode::isError, this::getException) + .bodyToMono(Void.class) + .block(); + } + + private Mono getException(ClientResponse response) { + return response + .bodyToMono(ApiErrorResponse.class) + .map(BotApiException::new); + } +} diff --git a/scrapper/src/main/java/edu/java/client/GithubClient.java b/scrapper/src/main/java/edu/java/scrapper/client/sources/GithubClient.java similarity index 74% rename from scrapper/src/main/java/edu/java/client/GithubClient.java rename to scrapper/src/main/java/edu/java/scrapper/client/sources/GithubClient.java index 007f4cf..365e794 100644 --- a/scrapper/src/main/java/edu/java/client/GithubClient.java +++ b/scrapper/src/main/java/edu/java/scrapper/client/sources/GithubClient.java @@ -1,9 +1,10 @@ -package edu.java.client; +package edu.java.scrapper.client.sources; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; -import edu.java.dto.GithubResponse; +import edu.java.scrapper.client.AbstractClient; +import edu.java.scrapper.client.sources.dto.GithubResponse; import java.util.Optional; import org.springframework.web.reactive.function.client.WebClientResponseException; import reactor.core.publisher.Mono; @@ -14,12 +15,12 @@ public GithubClient(String baseUrl, ObjectMapper mapper) { super(baseUrl, mapper); } - public Optional getUpdate(String author, String repository) throws ResponseException { + public Optional getUpdate(String owner, String repo) throws ResponseException { Mono jsonNodeMono = client.get() .uri(uriBuilder -> uriBuilder - .path("/repos/{author}/{repo}/events") + .path("/repos/{owner}/{repo}/events") .queryParam("per_page", 1) - .build(author, repository)) + .build(owner, repo)) .retrieve() .bodyToMono(JsonNode.class); try { diff --git a/scrapper/src/main/java/edu/java/client/ResponseException.java b/scrapper/src/main/java/edu/java/scrapper/client/sources/ResponseException.java similarity index 56% rename from scrapper/src/main/java/edu/java/client/ResponseException.java rename to scrapper/src/main/java/edu/java/scrapper/client/sources/ResponseException.java index c72808b..c28d33c 100644 --- a/scrapper/src/main/java/edu/java/client/ResponseException.java +++ b/scrapper/src/main/java/edu/java/scrapper/client/sources/ResponseException.java @@ -1,4 +1,4 @@ -package edu.java.client; +package edu.java.scrapper.client.sources; public class ResponseException extends Exception { } diff --git a/scrapper/src/main/java/edu/java/client/StackoverflowClient.java b/scrapper/src/main/java/edu/java/scrapper/client/sources/StackoverflowClient.java similarity index 89% rename from scrapper/src/main/java/edu/java/client/StackoverflowClient.java rename to scrapper/src/main/java/edu/java/scrapper/client/sources/StackoverflowClient.java index f9afbe5..7625599 100644 --- a/scrapper/src/main/java/edu/java/client/StackoverflowClient.java +++ b/scrapper/src/main/java/edu/java/scrapper/client/sources/StackoverflowClient.java @@ -1,9 +1,10 @@ -package edu.java.client; +package edu.java.scrapper.client.sources; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; -import edu.java.dto.StackoverflowResponse; +import edu.java.scrapper.client.AbstractClient; +import edu.java.scrapper.client.sources.dto.StackoverflowResponse; import java.util.Optional; import org.springframework.web.reactive.function.client.WebClientResponseException; import reactor.core.publisher.Mono; diff --git a/scrapper/src/main/java/edu/java/dto/GithubResponse.java b/scrapper/src/main/java/edu/java/scrapper/client/sources/dto/GithubResponse.java similarity index 91% rename from scrapper/src/main/java/edu/java/dto/GithubResponse.java rename to scrapper/src/main/java/edu/java/scrapper/client/sources/dto/GithubResponse.java index b122ce0..2ddb643 100644 --- a/scrapper/src/main/java/edu/java/dto/GithubResponse.java +++ b/scrapper/src/main/java/edu/java/scrapper/client/sources/dto/GithubResponse.java @@ -1,4 +1,4 @@ -package edu.java.dto; +package edu.java.scrapper.client.sources.dto; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import java.time.OffsetDateTime; diff --git a/scrapper/src/main/java/edu/java/dto/StackoverflowResponse.java b/scrapper/src/main/java/edu/java/scrapper/client/sources/dto/StackoverflowResponse.java similarity index 89% rename from scrapper/src/main/java/edu/java/dto/StackoverflowResponse.java rename to scrapper/src/main/java/edu/java/scrapper/client/sources/dto/StackoverflowResponse.java index 4a0a8d0..b3ac47e 100644 --- a/scrapper/src/main/java/edu/java/dto/StackoverflowResponse.java +++ b/scrapper/src/main/java/edu/java/scrapper/client/sources/dto/StackoverflowResponse.java @@ -1,4 +1,4 @@ -package edu.java.dto; +package edu.java.scrapper.client.sources.dto; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import java.time.OffsetDateTime; diff --git a/scrapper/src/main/java/edu/java/configuration/ApplicationConfig.java b/scrapper/src/main/java/edu/java/scrapper/configuration/ApplicationConfig.java similarity index 92% rename from scrapper/src/main/java/edu/java/configuration/ApplicationConfig.java rename to scrapper/src/main/java/edu/java/scrapper/configuration/ApplicationConfig.java index 1760267..7574e4e 100644 --- a/scrapper/src/main/java/edu/java/configuration/ApplicationConfig.java +++ b/scrapper/src/main/java/edu/java/scrapper/configuration/ApplicationConfig.java @@ -1,4 +1,4 @@ -package edu.java.configuration; +package edu.java.scrapper.configuration; import jakarta.validation.constraints.NotNull; import java.time.Duration; diff --git a/scrapper/src/main/java/edu/java/configuration/ClientConfig.java b/scrapper/src/main/java/edu/java/scrapper/configuration/ClientConfig.java similarity index 83% rename from scrapper/src/main/java/edu/java/configuration/ClientConfig.java rename to scrapper/src/main/java/edu/java/scrapper/configuration/ClientConfig.java index 7d36ea2..63595c0 100644 --- a/scrapper/src/main/java/edu/java/configuration/ClientConfig.java +++ b/scrapper/src/main/java/edu/java/scrapper/configuration/ClientConfig.java @@ -1,9 +1,9 @@ -package edu.java.configuration; +package edu.java.scrapper.configuration; import com.fasterxml.jackson.databind.ObjectMapper; -import edu.java.api.client.BotClient; -import edu.java.client.GithubClient; -import edu.java.client.StackoverflowClient; +import edu.java.scrapper.client.bot.BotClient; +import edu.java.scrapper.client.sources.GithubClient; +import edu.java.scrapper.client.sources.StackoverflowClient; import lombok.RequiredArgsConstructor; import org.apache.logging.log4j.util.Strings; import org.springframework.beans.factory.annotation.Value; @@ -41,9 +41,6 @@ public StackoverflowClient stackoverflowClient() { @Bean public BotClient botClient() { - return new BotClient( - botUrl, - objectMapper - ); + return new BotClient(botUrl, objectMapper); } } diff --git a/scrapper/src/main/java/edu/java/scrapper/configuration/LinkParserConfig.java b/scrapper/src/main/java/edu/java/scrapper/configuration/LinkParserConfig.java new file mode 100644 index 0000000..852f10f --- /dev/null +++ b/scrapper/src/main/java/edu/java/scrapper/configuration/LinkParserConfig.java @@ -0,0 +1,21 @@ +package edu.java.scrapper.configuration; + +import edu.java.dto.utils.LinkParser; +import edu.java.dto.utils.sources.parsers.GithubParser; +import edu.java.dto.utils.sources.parsers.SourceParser; +import edu.java.dto.utils.sources.parsers.StackoverflowParser; +import java.util.Set; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class LinkParserConfig { + + @Bean + public LinkParser linkParser() { + return new LinkParser(SourceParser.buildChain(Set.of( + new GithubParser(), + new StackoverflowParser() + )), false); + } +} diff --git a/scrapper/src/main/java/edu/java/configuration/ObjectMapperConfig.java b/scrapper/src/main/java/edu/java/scrapper/configuration/ObjectMapperConfig.java similarity index 94% rename from scrapper/src/main/java/edu/java/configuration/ObjectMapperConfig.java rename to scrapper/src/main/java/edu/java/scrapper/configuration/ObjectMapperConfig.java index 6d2f3be..daac430 100644 --- a/scrapper/src/main/java/edu/java/configuration/ObjectMapperConfig.java +++ b/scrapper/src/main/java/edu/java/scrapper/configuration/ObjectMapperConfig.java @@ -1,4 +1,4 @@ -package edu.java.configuration; +package edu.java.scrapper.configuration; import com.fasterxml.jackson.databind.Module; import com.fasterxml.jackson.databind.ObjectMapper; diff --git a/scrapper/src/main/java/edu/java/shedule/LinkUpdaterScheduler.java b/scrapper/src/main/java/edu/java/scrapper/shedule/LinkUpdaterScheduler.java similarity index 58% rename from scrapper/src/main/java/edu/java/shedule/LinkUpdaterScheduler.java rename to scrapper/src/main/java/edu/java/scrapper/shedule/LinkUpdaterScheduler.java index b04b193..b399eaf 100644 --- a/scrapper/src/main/java/edu/java/shedule/LinkUpdaterScheduler.java +++ b/scrapper/src/main/java/edu/java/scrapper/shedule/LinkUpdaterScheduler.java @@ -1,6 +1,8 @@ -package edu.java.shedule; +package edu.java.scrapper.shedule; +import edu.java.scrapper.api.service.LinkUpdater; import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Component; @@ -9,9 +11,15 @@ @ConditionalOnProperty(value = "app.scheduler.enable", matchIfMissing = true) @Slf4j public class LinkUpdaterScheduler { + private final LinkUpdater updater; + + @Autowired + public LinkUpdaterScheduler(LinkUpdater updater) { + this.updater = updater; + } @Scheduled(fixedDelayString = "${app.scheduler.interval}") public void update() { - log.info("Обновление"); + updater.update(); } } diff --git a/scrapper/src/main/java/edu/java/scrapper/shedule/update/dto/Update.java b/scrapper/src/main/java/edu/java/scrapper/shedule/update/dto/Update.java new file mode 100644 index 0000000..fee75d9 --- /dev/null +++ b/scrapper/src/main/java/edu/java/scrapper/shedule/update/dto/Update.java @@ -0,0 +1,13 @@ +package edu.java.scrapper.shedule.update.dto; + +import java.time.OffsetDateTime; +import lombok.Getter; + +@Getter +public class Update { + protected final OffsetDateTime createdAt; + + public Update(OffsetDateTime createdAt) { + this.createdAt = createdAt; + } +} diff --git a/scrapper/src/main/java/edu/java/scrapper/shedule/update/sources/GithubUpdater.java b/scrapper/src/main/java/edu/java/scrapper/shedule/update/sources/GithubUpdater.java new file mode 100644 index 0000000..dd1e1b3 --- /dev/null +++ b/scrapper/src/main/java/edu/java/scrapper/shedule/update/sources/GithubUpdater.java @@ -0,0 +1,22 @@ +package edu.java.scrapper.shedule.update.sources; + +import edu.java.dto.utils.sources.info.GithubInfo; +import edu.java.dto.utils.sources.info.SourceInfo; +import edu.java.scrapper.client.sources.ResponseException; +import edu.java.scrapper.client.sources.dto.GithubResponse; +import edu.java.scrapper.configuration.ClientConfig; +import edu.java.scrapper.shedule.update.dto.Update; +import java.util.Optional; + +public class GithubUpdater extends SourceUpdater { + public GithubUpdater(ClientConfig config) { + super(config); + } + + @Override + public Optional getUpdate(SourceInfo sourceInfo) throws ResponseException { + GithubInfo info = (GithubInfo) sourceInfo; + Optional response = config.githubClient().getUpdate(info.getOwner(), info.getRepo()); + return response.map(stackoverflowResponse -> new Update(stackoverflowResponse.createdAt())); + } +} diff --git a/scrapper/src/main/java/edu/java/scrapper/shedule/update/sources/SourceUpdater.java b/scrapper/src/main/java/edu/java/scrapper/shedule/update/sources/SourceUpdater.java new file mode 100644 index 0000000..a3c5e57 --- /dev/null +++ b/scrapper/src/main/java/edu/java/scrapper/shedule/update/sources/SourceUpdater.java @@ -0,0 +1,28 @@ +package edu.java.scrapper.shedule.update.sources; + +import edu.java.dto.utils.sources.info.GithubInfo; +import edu.java.dto.utils.sources.info.SourceInfo; +import edu.java.dto.utils.sources.info.StackoverflowInfo; +import edu.java.scrapper.client.sources.ResponseException; +import edu.java.scrapper.configuration.ClientConfig; +import edu.java.scrapper.shedule.update.dto.Update; +import java.util.Optional; + +public abstract class SourceUpdater { + protected final ClientConfig config; + + protected SourceUpdater(ClientConfig config) { + this.config = config; + } + + public abstract Optional getUpdate(SourceInfo sourceInfo) throws ResponseException; + + public static SourceUpdater getUpdaterForSource(ClientConfig config, SourceInfo sourceInfo) { + if (sourceInfo instanceof GithubInfo) { + return new GithubUpdater(config); + } else if (sourceInfo instanceof StackoverflowInfo) { + return new StackoverflowUpdater(config); + } + return null; + } +} diff --git a/scrapper/src/main/java/edu/java/scrapper/shedule/update/sources/StackoverflowUpdater.java b/scrapper/src/main/java/edu/java/scrapper/shedule/update/sources/StackoverflowUpdater.java new file mode 100644 index 0000000..b4b6a19 --- /dev/null +++ b/scrapper/src/main/java/edu/java/scrapper/shedule/update/sources/StackoverflowUpdater.java @@ -0,0 +1,22 @@ +package edu.java.scrapper.shedule.update.sources; + +import edu.java.dto.utils.sources.info.SourceInfo; +import edu.java.dto.utils.sources.info.StackoverflowInfo; +import edu.java.scrapper.client.sources.ResponseException; +import edu.java.scrapper.client.sources.dto.StackoverflowResponse; +import edu.java.scrapper.configuration.ClientConfig; +import edu.java.scrapper.shedule.update.dto.Update; +import java.util.Optional; + +public class StackoverflowUpdater extends SourceUpdater { + public StackoverflowUpdater(ClientConfig config) { + super(config); + } + + @Override + public Optional getUpdate(SourceInfo sourceInfo) throws ResponseException { + StackoverflowInfo info = (StackoverflowInfo) sourceInfo; + Optional response = config.stackoverflowClient().getUpdate(info.getQuestionId()); + return response.map(stackoverflowResponse -> new Update(stackoverflowResponse.lastActivityDate())); + } +} diff --git a/scrapper/src/main/resources/application.yml b/scrapper/src/main/resources/application.yml index 8fa269c..170d0be 100644 --- a/scrapper/src/main/resources/application.yml +++ b/scrapper/src/main/resources/application.yml @@ -2,7 +2,7 @@ app: scheduler: enable: true interval: 10000 - force-check-delay: 10000 + force-check-delay: 300000 resources: base-url: @@ -14,9 +14,12 @@ spring: application: name: scrapper liquibase: - enabled: true - autoconfigure: - exclude: org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration + enabled: false + datasource: + driver-class-name: org.postgresql.Driver + url: "jdbc:postgresql://localhost:5433/scrapper" + username: "postgres" + password: "postgres" jackson: property-naming-strategy: SNAKE_CASE diff --git a/scrapper/src/test/java/edu/java/scrapper/api/domain/repository/ChatRepositoryTest.java b/scrapper/src/test/java/edu/java/scrapper/api/domain/repository/ChatRepositoryTest.java new file mode 100644 index 0000000..77c65db --- /dev/null +++ b/scrapper/src/test/java/edu/java/scrapper/api/domain/repository/ChatRepositoryTest.java @@ -0,0 +1,75 @@ +package edu.java.scrapper.api.domain.repository; + +import edu.java.scrapper.IntegrationTest; +import edu.java.scrapper.api.domain.dto.Chat; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.springframework.dao.DuplicateKeyException; +import org.springframework.test.annotation.Rollback; +import org.springframework.transaction.annotation.Transactional; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +public abstract class ChatRepositoryTest extends IntegrationTest { + private static final long TG_ID = 11111; + private final ChatRepository repo; + + protected ChatRepositoryTest(ChatRepository repo) { + this.repo = repo; + } + + @Test + @Transactional + @Rollback + void addTest() { + Chat actual = repo.add(TG_ID); + Assertions.assertEquals(TG_ID, actual.tgId()); + } + + @Test + @Transactional + @Rollback + void addDuplicateExceptionTest() { + repo.add(TG_ID); + Assertions.assertThrows(DuplicateKeyException.class, () -> repo.add(TG_ID)); + } + + @Test + @Transactional + @Rollback + void findByTgIdTest() { + Chat expected = repo.add(TG_ID); + Chat actual = repo.findByTgId(TG_ID).get(); + Assertions.assertEquals(expected, actual); + } + + @Test + @Transactional + @Rollback + void findByTgIdNotExistTest() { + Optional actual = repo.findByTgId(TG_ID); + Assertions.assertTrue(actual.isEmpty()); + } + + @Test + @Transactional + @Rollback + void deleteTest() { + repo.add(TG_ID); + repo.remove(TG_ID); + Optional actual = repo.findByTgId(TG_ID); + Assertions.assertTrue(actual.isEmpty()); + } + + @Test + @Transactional + @Rollback + void findAllTest() { + List expected = new ArrayList<>(); + expected.add(repo.add(TG_ID)); + expected.add(repo.add(22222)); + List actual = repo.findAll(); + Assertions.assertEquals(expected, actual); + } +} diff --git a/scrapper/src/test/java/edu/java/scrapper/api/domain/repository/LinkRepositoryTest.java b/scrapper/src/test/java/edu/java/scrapper/api/domain/repository/LinkRepositoryTest.java new file mode 100644 index 0000000..457c069 --- /dev/null +++ b/scrapper/src/test/java/edu/java/scrapper/api/domain/repository/LinkRepositoryTest.java @@ -0,0 +1,88 @@ +package edu.java.scrapper.api.domain.repository; + +import edu.java.scrapper.IntegrationTest; +import edu.java.scrapper.api.domain.dto.Link; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.springframework.dao.DuplicateKeyException; +import org.springframework.test.annotation.Rollback; +import org.springframework.transaction.annotation.Transactional; +import java.net.URI; +import java.time.OffsetDateTime; +import java.time.ZoneOffset; +import java.time.temporal.ChronoUnit; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +public abstract class LinkRepositoryTest extends IntegrationTest { + private static final URI URL = + URI.create("https://github.com/cyberpanncake/Torzhkova-Tinkoff-JavaBackendCourse-Spring/"); + private static final OffsetDateTime NOW = OffsetDateTime.now(ZoneOffset.UTC).truncatedTo(ChronoUnit.SECONDS); + private final LinkRepository repo; + + protected LinkRepositoryTest(LinkRepository repo) { + this.repo = repo; + } + + @Test + @Transactional + @Rollback + void addTest() { + Link expected = new Link(null, URL, NOW, NOW); + Link actual = repo.add(expected); + Assertions.assertEquals(expected.url(), actual.url()); + } + + @Test + @Transactional + @Rollback + void addDuplicateExceptionTest() { + Link link = new Link(null, URL, NOW, NOW); + repo.add(link); + Assertions.assertThrows(DuplicateKeyException.class, () -> repo.add(link)); + } + + @Test + @Transactional + @Rollback + void findByUrlTest() { + Link link = new Link(null, URL, NOW, NOW); + Link expected = repo.add(link); + Link actual = repo.findByUrl(URL).get(); + Assertions.assertEquals(expected, actual); + } + + @Test + @Transactional + @Rollback + void findByUrlNotExistTest() { + Optional actual = repo.findByUrl(URL); + Assertions.assertTrue(actual.isEmpty()); + } + + @Test + @Transactional + @Rollback + void deleteTest() { + Link link = new Link(null, URL, NOW, NOW); + repo.add(link); + repo.remove(URL); + Optional actual = repo.findByUrl(URL); + Assertions.assertTrue(actual.isEmpty()); + } + + @Test + @Transactional + @Rollback + void findAllTest() { + Link link1 = new Link(null, URL, NOW, NOW); + URI url2 = URI.create("https://github.com/cyberpanncake/Torzhkova-Tinkoff-JavaBackendCourse/"); + Link link2 = new Link(null, url2, NOW, NOW); + List expected = new ArrayList<>(); + expected.add(repo.add(link1)); + expected.add(repo.add(link2)); + List actual = repo.findAll(); + Assertions.assertEquals(expected, actual); + } +} diff --git a/scrapper/src/test/java/edu/java/scrapper/api/domain/repository/SubscriptionRepositoryTest.java b/scrapper/src/test/java/edu/java/scrapper/api/domain/repository/SubscriptionRepositoryTest.java new file mode 100644 index 0000000..30ca69d --- /dev/null +++ b/scrapper/src/test/java/edu/java/scrapper/api/domain/repository/SubscriptionRepositoryTest.java @@ -0,0 +1,109 @@ +package edu.java.scrapper.api.domain.repository; + +import edu.java.scrapper.IntegrationTest; +import edu.java.scrapper.api.domain.dto.Chat; +import edu.java.scrapper.api.domain.dto.Link; +import edu.java.scrapper.api.domain.dto.Subscription; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.springframework.dao.DuplicateKeyException; +import org.springframework.test.annotation.Rollback; +import org.springframework.transaction.annotation.Transactional; +import java.net.URI; +import java.time.OffsetDateTime; +import java.time.ZoneOffset; +import java.time.temporal.ChronoUnit; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +public abstract class SubscriptionRepositoryTest extends IntegrationTest { + private static final long TG_ID = 11111; + private static final URI URL = + URI.create("https://github.com/cyberpanncake/Torzhkova-Tinkoff-JavaBackendCourse-Spring/"); + private static final OffsetDateTime NOW = OffsetDateTime.now(ZoneOffset.UTC).truncatedTo(ChronoUnit.SECONDS); + private final ChatRepository chatRepo; + private final LinkRepository linkRepo; + private final SubscriptionRepository repo; + + protected SubscriptionRepositoryTest(ChatRepository chatRepo, + LinkRepository linkRepo, SubscriptionRepository repo) { + this.chatRepo = chatRepo; + this.linkRepo = linkRepo; + this.repo = repo; + } + + @Test + @Transactional + @Rollback + void addTest() { + Chat chat = chatRepo.add(TG_ID); + Link link = linkRepo.add(new Link(null, URL, NOW, NOW)); + Subscription expected = new Subscription(chat.id(), link.id()); + Subscription actual = repo.add(expected); + Assertions.assertEquals(expected, actual); + } + + @Test + @Transactional + @Rollback + void addDuplicateExceptionTest() { + Chat chat = chatRepo.add(TG_ID); + Link link = linkRepo.add(new Link(null, URL, NOW, NOW)); + Subscription subscription = new Subscription(chat.id(), link.id()); + repo.add(subscription); + Assertions.assertThrows(DuplicateKeyException.class, () -> repo.add(subscription)); + } + + @Test + @Transactional + @Rollback + void findTest() { + Chat chat = chatRepo.add(TG_ID); + Link link = linkRepo.add(new Link(null, URL, NOW, NOW)); + Subscription subscription = new Subscription(chat.id(), link.id()); + Subscription expected = repo.add(subscription); + Subscription actual = repo.find(TG_ID, URL).get(); + Assertions.assertEquals(expected, actual); + } + + @Test + @Transactional + @Rollback + void findNotExistTest() { + Chat chat = chatRepo.add(TG_ID); + Link link = linkRepo.add(new Link(null, URL, NOW, NOW)); + Subscription subscription = new Subscription(chat.id(), link.id()); + Optional actual = repo.find(TG_ID, URL); + Assertions.assertTrue(actual.isEmpty()); + } + + @Test + @Transactional + @Rollback + void deleteTest() { + Chat chat = chatRepo.add(TG_ID); + Link link = linkRepo.add(new Link(null, URL, NOW, NOW)); + Subscription subscription = new Subscription(chat.id(), link.id()); + repo.add(subscription); + repo.remove(subscription); + Optional actual = repo.find(TG_ID, URL); + Assertions.assertTrue(actual.isEmpty()); + } + + @Test + @Transactional + @Rollback + void findAllTest() { + Chat chat1 = chatRepo.add(TG_ID); + Chat chat2 = chatRepo.add(22222); + Link link = linkRepo.add(new Link(null, URL, NOW, NOW)); + Subscription subscription1 = new Subscription(chat1.id(), link.id()); + Subscription subscription2 = new Subscription(chat2.id(), link.id()); + List expected = new ArrayList<>(); + expected.add(repo.add(subscription1)); + expected.add(repo.add(subscription2)); + List actual = repo.findAll(); + Assertions.assertEquals(expected, actual); + } +} diff --git a/scrapper/src/test/java/edu/java/scrapper/api/domain/repository/jdbc/JdbcChatRepositoryTest.java b/scrapper/src/test/java/edu/java/scrapper/api/domain/repository/jdbc/JdbcChatRepositoryTest.java new file mode 100644 index 0000000..82f2c30 --- /dev/null +++ b/scrapper/src/test/java/edu/java/scrapper/api/domain/repository/jdbc/JdbcChatRepositoryTest.java @@ -0,0 +1,14 @@ +package edu.java.scrapper.api.domain.repository.jdbc; + +import edu.java.scrapper.api.domain.repository.ChatRepositoryTest; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class JdbcChatRepositoryTest extends ChatRepositoryTest { + + @Autowired + protected JdbcChatRepositoryTest(JdbcChatRepository repo) { + super(repo); + } +} diff --git a/scrapper/src/test/java/edu/java/scrapper/api/domain/repository/jdbc/JdbcLinkRepositoryTest.java b/scrapper/src/test/java/edu/java/scrapper/api/domain/repository/jdbc/JdbcLinkRepositoryTest.java new file mode 100644 index 0000000..367d6fc --- /dev/null +++ b/scrapper/src/test/java/edu/java/scrapper/api/domain/repository/jdbc/JdbcLinkRepositoryTest.java @@ -0,0 +1,14 @@ +package edu.java.scrapper.api.domain.repository.jdbc; + +import edu.java.scrapper.api.domain.repository.LinkRepositoryTest; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class JdbcLinkRepositoryTest extends LinkRepositoryTest { + + @Autowired + protected JdbcLinkRepositoryTest(JdbcLinkRepository repo) { + super(repo); + } +} diff --git a/scrapper/src/test/java/edu/java/scrapper/api/domain/repository/jdbc/JdbcSubscriptionRepositoryTest.java b/scrapper/src/test/java/edu/java/scrapper/api/domain/repository/jdbc/JdbcSubscriptionRepositoryTest.java new file mode 100644 index 0000000..9f95afa --- /dev/null +++ b/scrapper/src/test/java/edu/java/scrapper/api/domain/repository/jdbc/JdbcSubscriptionRepositoryTest.java @@ -0,0 +1,15 @@ +package edu.java.scrapper.api.domain.repository.jdbc; + +import edu.java.scrapper.api.domain.repository.SubscriptionRepositoryTest; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class JdbcSubscriptionRepositoryTest extends SubscriptionRepositoryTest { + + @Autowired + protected JdbcSubscriptionRepositoryTest(JdbcSubscriptionRepository repo, JdbcChatRepository chatRepo, + JdbcLinkRepository linkRepo) { + super(chatRepo, linkRepo, repo); + } +} diff --git a/scrapper/src/test/java/edu/java/client/GithubClientTest.java b/scrapper/src/test/java/edu/java/scrapper/client/GithubClientTest.java similarity index 94% rename from scrapper/src/test/java/edu/java/client/GithubClientTest.java rename to scrapper/src/test/java/edu/java/scrapper/client/GithubClientTest.java index ded166b..277b85e 100644 --- a/scrapper/src/test/java/edu/java/client/GithubClientTest.java +++ b/scrapper/src/test/java/edu/java/scrapper/client/GithubClientTest.java @@ -1,10 +1,12 @@ -package edu.java.client; +package edu.java.scrapper.client; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; import com.github.tomakehurst.wiremock.WireMockServer; -import edu.java.configuration.ObjectMapperConfig; -import edu.java.dto.GithubResponse; +import edu.java.scrapper.client.sources.GithubClient; +import edu.java.scrapper.client.sources.ResponseException; +import edu.java.scrapper.configuration.ObjectMapperConfig; +import edu.java.scrapper.client.sources.dto.GithubResponse; import java.util.List; import java.util.Map; import java.util.Optional; diff --git a/scrapper/src/test/java/edu/java/scrapper/client/ObjectMapperTest.java b/scrapper/src/test/java/edu/java/scrapper/client/ObjectMapperTest.java new file mode 100644 index 0000000..6af27ca --- /dev/null +++ b/scrapper/src/test/java/edu/java/scrapper/client/ObjectMapperTest.java @@ -0,0 +1,28 @@ +package edu.java.scrapper.client; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import edu.java.scrapper.configuration.ObjectMapperConfig; +import edu.java.scrapper.client.sources.dto.GithubResponse; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import java.time.OffsetDateTime; + +public class ObjectMapperTest { + + private final ObjectMapper mapper; + + public ObjectMapperTest() { + this.mapper = new ObjectMapperConfig().objectMapper(); + } + + @Test + void objectMapperTest() throws JsonProcessingException { + GithubResponse object = new GithubResponse(0L, "example", + new GithubResponse.Actor("cyberpanncake"), + new GithubResponse.Repository("Torzhkova-Tinkoff-JavaBackendCourse-Spring"), + OffsetDateTime.now()); + String json = mapper.writeValueAsString(object); + Assertions.assertTrue(json.contains("created_at")); + } +} diff --git a/scrapper/src/test/java/edu/java/client/StackoverflowClientTest.java b/scrapper/src/test/java/edu/java/scrapper/client/StackoverflowClientTest.java similarity index 94% rename from scrapper/src/test/java/edu/java/client/StackoverflowClientTest.java rename to scrapper/src/test/java/edu/java/scrapper/client/StackoverflowClientTest.java index 95e2757..de5909d 100644 --- a/scrapper/src/test/java/edu/java/client/StackoverflowClientTest.java +++ b/scrapper/src/test/java/edu/java/scrapper/client/StackoverflowClientTest.java @@ -1,10 +1,12 @@ -package edu.java.client; +package edu.java.scrapper.client; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.github.tomakehurst.wiremock.WireMockServer; -import edu.java.configuration.ObjectMapperConfig; -import edu.java.dto.StackoverflowResponse; +import edu.java.scrapper.client.sources.ResponseException; +import edu.java.scrapper.client.sources.StackoverflowClient; +import edu.java.scrapper.configuration.ObjectMapperConfig; +import edu.java.scrapper.client.sources.dto.StackoverflowResponse; import java.util.Map; import java.util.Optional; import org.junit.jupiter.api.AfterEach;