diff --git a/README.md b/README.md index 18a246e..d36813d 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,2 @@ # java-explore-with-me -Template repository for ExploreWithMe project. +Итоговый проект учебного курса. май 2025. гр. 53. diff --git a/docker-compose.yml b/docker-compose.yml index be96142..a02f49e 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,14 +1,56 @@ +version: '3.1' services: + stats-db: + image: postgres:16.1 + container_name: postgres-stat + ports: + - "5432:5432" + environment: + - POSTGRES_PASSWORD=statdb + - POSTGRES_USER=statdb + - POSTGRES_DB=statdb + healthcheck: + test: pg_isready -q -d $$POSTGRES_DB -U $$POSTGRES_USER + timeout: 5s + interval: 5s + retries: 10 + stats-server: + build: stats-server + image: stats-server + container_name: stats-server ports: - "9090:9090" - - stats-db: - image: postgres:16.1 + depends_on: + - stats-db + environment: + - SPRING_DATASOURCE_URL=jdbc:postgresql://stats-db:5432/statdb + - SPRING_DATASOURCE_USERNAME=statdb + - SPRING_DATASOURCE_PASSWORD=statdb ewm-service: + build: ewm-service + image: ewm-service + container_name: ewm-service ports: - "8080:8080" + depends_on: + - stats-server + environment: + - STATSERVER_URL=http://stats-server:9090 ewm-db: image: postgres:16.1 + container_name: postgres-ewm + ports: + - "5434:5434" + environment: + - POSTGRES_PASSWORD=statewm + - POSTGRES_USER=statewm + - POSTGRES_DB=statewm + healthcheck: + test: pg_isready -q -d $$POSTGRES_DB -U $$POSTGRES_USER + timeout: 5s + interval: 5s + retries: 10 + diff --git a/ewm-service/Dockerfile b/ewm-service/Dockerfile new file mode 100644 index 0000000..0ff1817 --- /dev/null +++ b/ewm-service/Dockerfile @@ -0,0 +1,5 @@ +FROM eclipse-temurin:21-jre-jammy +VOLUME /tmp +ARG JAR_FILE=target/*.jar +COPY ${JAR_FILE} app.jar +ENTRYPOINT ["sh", "-c", "java ${JAVA_OPTS} -jar /app.jar"] \ No newline at end of file diff --git a/ewm-service/README.md b/ewm-service/README.md new file mode 100644 index 0000000..f6eea04 --- /dev/null +++ b/ewm-service/README.md @@ -0,0 +1 @@ +# ewm-service \ No newline at end of file diff --git a/ewm-service/pom.xml b/ewm-service/pom.xml new file mode 100644 index 0000000..9eb9b4f --- /dev/null +++ b/ewm-service/pom.xml @@ -0,0 +1,79 @@ + + + 4.0.0 + + ru.practicum + explore-with-me + 0.0.1-SNAPSHOT + + + ewm-service + + + + org.springframework.boot + spring-boot-starter-web + + + + org.springframework.boot + spring-boot-starter-validation + + + + org.springframework.boot + spring-boot-starter-actuator + + + + org.springframework.boot + spring-boot-configuration-processor + true + + + + org.apache.httpcomponents.client5 + httpclient5 + + + + org.projectlombok + lombok + true + + + + ru.practicum + stat-dto + 0.0.1-SNAPSHOT + compile + + + + ru.practicum + stat-client + 0.0.1-SNAPSHOT + compile + + + + + + 21 + 21 + UTF-8 + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + + \ No newline at end of file diff --git a/ewm-service/src/main/java/ru/practicum/evmsevice/EwmServiceApp.java b/ewm-service/src/main/java/ru/practicum/evmsevice/EwmServiceApp.java new file mode 100644 index 0000000..20b8584 --- /dev/null +++ b/ewm-service/src/main/java/ru/practicum/evmsevice/EwmServiceApp.java @@ -0,0 +1,11 @@ +package ru.practicum.evmsevice; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class EwmServiceApp { + public static void main(String[] args) { + SpringApplication.run(EwmServiceApp.class, args); + } +} diff --git a/ewm-service/src/main/java/ru/practicum/evmsevice/client/StatsClient.java b/ewm-service/src/main/java/ru/practicum/evmsevice/client/StatsClient.java new file mode 100644 index 0000000..811b2a1 --- /dev/null +++ b/ewm-service/src/main/java/ru/practicum/evmsevice/client/StatsClient.java @@ -0,0 +1,38 @@ +package ru.practicum.evmsevice.client; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.web.client.RestTemplateBuilder; +import org.springframework.http.HttpMethod; +import org.springframework.http.ResponseEntity; +import org.springframework.http.client.HttpComponentsClientHttpRequestFactory; +import org.springframework.stereotype.Component; +import org.springframework.web.util.DefaultUriBuilderFactory; +import ru.practicum.statclient.BaseClient; +import ru.practicum.statdto.HitDto; + +import java.util.Map; + +@Component +public class StatsClient extends BaseClient { + private static final String PREFIX_HIT = "/hit"; + private static final String PREFIX_STATS = "/stats"; + + @Autowired + public StatsClient(@Value("${statserver.url}") String serverUrl, RestTemplateBuilder builder) { + super( + builder + .uriTemplateHandler(new DefaultUriBuilderFactory(serverUrl)) + .requestFactory(() -> new HttpComponentsClientHttpRequestFactory()) + .build() + ); + } + + public void post(HitDto dto) { + makeAndSendRequest(HttpMethod.POST, PREFIX_HIT, null, dto); + } + + public ResponseEntity get(Map parameters) { + return makeAndSendRequest(HttpMethod.GET, PREFIX_STATS, parameters, null); + } +} diff --git a/ewm-service/src/main/java/ru/practicum/evmsevice/controller/TestClientController.java b/ewm-service/src/main/java/ru/practicum/evmsevice/controller/TestClientController.java new file mode 100644 index 0000000..c817743 --- /dev/null +++ b/ewm-service/src/main/java/ru/practicum/evmsevice/controller/TestClientController.java @@ -0,0 +1,51 @@ +package ru.practicum.evmsevice.controller; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; +import ru.practicum.evmsevice.client.StatsClient; +import ru.practicum.statdto.HitDto; + +import java.util.HashMap; +import java.util.Map; + +/** + * Класс для проверки работы клиента сервера посещений + */ +@Slf4j +@RequiredArgsConstructor +@RestController +@RequestMapping +public class TestClientController { + private final StatsClient statsClient; + + @PostMapping("/hit") + @ResponseStatus(HttpStatus.CREATED) + public void hit(@RequestBody HitDto dto) { + log.info("Поступила информация о посещении : " + dto.toString()); + statsClient.post(dto); + } + + @GetMapping("/stats") + @ResponseStatus(HttpStatus.OK) + public ResponseEntity getStats( + @RequestParam(required = false) String start, + @RequestParam(required = false) String end, + @RequestParam(required = false) String uris, + @RequestParam(defaultValue = "false") Boolean unique, + @RequestParam(defaultValue = "10") Integer size) { + log.info("Запрашивается информация о посещении эндпоинта {} с {} до {}.", uris, start, end); + + Map parameters = new HashMap<>(); + if (start != null) parameters.put("start", start); + if (end != null) parameters.put("end", end); + if (uris != null) parameters.put("uris", uris); + if (unique != null) parameters.put("unique", unique); + if (size != null) parameters.put("size", size); + ResponseEntity response = statsClient.get(parameters); + return response; + } +} + diff --git a/ewm-service/src/main/resources/application.properties b/ewm-service/src/main/resources/application.properties new file mode 100644 index 0000000..39a76d7 --- /dev/null +++ b/ewm-service/src/main/resources/application.properties @@ -0,0 +1,2 @@ +server.port=8080 +statserver.url=http://localhost:9090 \ No newline at end of file diff --git a/pom.xml b/pom.xml index b15acb2..9ee476e 100644 --- a/pom.xml +++ b/pom.xml @@ -11,8 +11,12 @@ Explore With Me + + stats-server + ewm-service + - ru.practicum + ru.practicum explore-with-me 0.0.1-SNAPSHOT pom diff --git a/stats-server/Dockerfile b/stats-server/Dockerfile new file mode 100644 index 0000000..b3fe293 --- /dev/null +++ b/stats-server/Dockerfile @@ -0,0 +1,5 @@ +FROM eclipse-temurin:21-jre-jammy +VOLUME /tmp +ARG JAR_FILE=stat-svc/target/*.jar +COPY ${JAR_FILE} app.jar +ENTRYPOINT ["sh", "-c", "java ${JAVA_OPTS} -jar /app.jar"] \ No newline at end of file diff --git a/stats-server/README.md b/stats-server/README.md new file mode 100644 index 0000000..32f6d2a --- /dev/null +++ b/stats-server/README.md @@ -0,0 +1,3 @@ +# stats-server + +модуль сервера статистики посещений diff --git a/stats-server/pom.xml b/stats-server/pom.xml new file mode 100644 index 0000000..978cf94 --- /dev/null +++ b/stats-server/pom.xml @@ -0,0 +1,41 @@ + + + 4.0.0 + + ru.practicum + explore-with-me + 0.0.1-SNAPSHOT + + + stats-server + pom + + stat-dto + stat-svc + stat-client + + + + + + org.springframework.boot + spring-boot-starter-actuator + + + + org.projectlombok + lombok + true + + + + + + 21 + 21 + UTF-8 + + + \ No newline at end of file diff --git a/stats-server/stat-client/README.md b/stats-server/stat-client/README.md new file mode 100644 index 0000000..c1607f7 --- /dev/null +++ b/stats-server/stat-client/README.md @@ -0,0 +1,3 @@ +# stat-client + +модуль описания классов для подключения клиента. \ No newline at end of file diff --git a/stats-server/stat-client/pom.xml b/stats-server/stat-client/pom.xml new file mode 100644 index 0000000..bf15958 --- /dev/null +++ b/stats-server/stat-client/pom.xml @@ -0,0 +1,56 @@ + + + 4.0.0 + + ru.practicum + stats-server + 0.0.1-SNAPSHOT + + + stat-client + + + + org.springframework.boot + spring-boot-starter-web + + + + org.springframework.boot + spring-boot-starter-validation + + + + org.springframework.boot + spring-boot-starter-actuator + + + + org.springframework.boot + spring-boot-configuration-processor + true + + + + org.apache.httpcomponents.client5 + httpclient5 + + + + ru.practicum + stat-dto + 0.0.1-SNAPSHOT + compile + + + + + + 21 + 21 + UTF-8 + + + \ No newline at end of file diff --git a/stats-server/stat-client/src/main/java/ru/practicum/statclient/BaseClient.java b/stats-server/stat-client/src/main/java/ru/practicum/statclient/BaseClient.java new file mode 100644 index 0000000..28ce7fc --- /dev/null +++ b/stats-server/stat-client/src/main/java/ru/practicum/statclient/BaseClient.java @@ -0,0 +1,64 @@ +package ru.practicum.statclient; + +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpMethod; +import org.springframework.http.ResponseEntity; +import org.springframework.web.client.HttpStatusCodeException; +import org.springframework.web.client.RestTemplate; + +import java.util.Map; + +public class BaseClient { + protected final RestTemplate rest; + + public BaseClient(RestTemplate rest) { + this.rest = rest; + } + + private static ResponseEntity prepareClientResponse(ResponseEntity response) { + if (response.getStatusCode().is2xxSuccessful()) { + return response; + } + ResponseEntity.BodyBuilder responseBuilder = ResponseEntity.status(response.getStatusCode()); + if (response.hasBody()) { + return responseBuilder.body(response.getBody()); + } + return responseBuilder.build(); + } + + /** + * Формируем и отправляем http запрос на сервер + * + * @param method - метод запроса + * @param path - эндпоинт + * @param parameters - карта параметров + * @param body - тело запроса + * @param - тип объекта тела запроса + * @return - ResponseEntity + */ + protected ResponseEntity makeAndSendRequest(HttpMethod method, + String path, + Map parameters, + T body) { + HttpEntity requestEntity = new HttpEntity<>(body); + ResponseEntity serverResponse; + try { + if (parameters != null) { + StringBuilder stringParametrs = new StringBuilder(path); + stringParametrs.append("?"); + for (String key : parameters.keySet()) { + stringParametrs.append(key); + stringParametrs.append("={"); + stringParametrs.append(key); + stringParametrs.append("}&"); + } + serverResponse = rest.exchange(stringParametrs.toString(), method, requestEntity, Object.class, parameters); + } else { + serverResponse = rest.exchange(path, method, requestEntity, Object.class); + } + } catch (HttpStatusCodeException e) { + return ResponseEntity.status(e.getStatusCode()).body(e.getResponseBodyAsByteArray()); + } + return prepareClientResponse(serverResponse); + } +} diff --git a/stats-server/stat-dto/README.md b/stats-server/stat-dto/README.md new file mode 100644 index 0000000..7698437 --- /dev/null +++ b/stats-server/stat-dto/README.md @@ -0,0 +1,3 @@ +# stat-dto + +модуль описания объектов передачи данных \ No newline at end of file diff --git a/stats-server/stat-dto/pom.xml b/stats-server/stat-dto/pom.xml new file mode 100644 index 0000000..a5963ff --- /dev/null +++ b/stats-server/stat-dto/pom.xml @@ -0,0 +1,33 @@ + + + 4.0.0 + + ru.practicum + stats-server + 0.0.1-SNAPSHOT + + + stat-dto + + + + org.springframework.boot + spring-boot-starter-actuator + + + + com.fasterxml.jackson.datatype + jackson-datatype-jsr310 + 2.9.0 + + + + + 21 + 21 + UTF-8 + + + \ No newline at end of file diff --git a/stats-server/stat-dto/src/main/java/ru/practicum/statdto/ErrorMessage.java b/stats-server/stat-dto/src/main/java/ru/practicum/statdto/ErrorMessage.java new file mode 100644 index 0000000..0300d40 --- /dev/null +++ b/stats-server/stat-dto/src/main/java/ru/practicum/statdto/ErrorMessage.java @@ -0,0 +1,10 @@ +package ru.practicum.statdto; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public class ErrorMessage { + private String error; +} \ No newline at end of file diff --git a/stats-server/stat-dto/src/main/java/ru/practicum/statdto/HitDto.java b/stats-server/stat-dto/src/main/java/ru/practicum/statdto/HitDto.java new file mode 100644 index 0000000..66ce613 --- /dev/null +++ b/stats-server/stat-dto/src/main/java/ru/practicum/statdto/HitDto.java @@ -0,0 +1,19 @@ +package ru.practicum.statdto; + +import com.fasterxml.jackson.annotation.JsonFormat; +import lombok.*; + +import java.time.LocalDateTime; + +@Setter +@Getter +@NoArgsConstructor +@AllArgsConstructor +@ToString +public class HitDto { + private String app; + private String uri; + private String ip; + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private LocalDateTime timestamp; +} diff --git a/stats-server/stat-dto/src/main/java/ru/practicum/statdto/StatsDto.java b/stats-server/stat-dto/src/main/java/ru/practicum/statdto/StatsDto.java new file mode 100644 index 0000000..9922a8e --- /dev/null +++ b/stats-server/stat-dto/src/main/java/ru/practicum/statdto/StatsDto.java @@ -0,0 +1,14 @@ +package ru.practicum.statdto; + +import lombok.*; + +@Setter +@Getter +@NoArgsConstructor +@AllArgsConstructor +@ToString +public class StatsDto { + private String app; + private String uri; + private Integer hits; +} diff --git a/stats-server/stat-svc/README.md b/stats-server/stat-svc/README.md new file mode 100644 index 0000000..2a6a48f --- /dev/null +++ b/stats-server/stat-svc/README.md @@ -0,0 +1,3 @@ +# stat-svc + +модуль описания классов сервиса статистики посещений diff --git a/stats-server/stat-svc/pom.xml b/stats-server/stat-svc/pom.xml new file mode 100644 index 0000000..3d2b1cd --- /dev/null +++ b/stats-server/stat-svc/pom.xml @@ -0,0 +1,86 @@ + + + 4.0.0 + + ru.practicum + stats-server + 0.0.1-SNAPSHOT + + + stat-svc + + + + + org.springframework.boot + spring-boot-starter-actuator + + + + org.springframework.boot + spring-boot-starter-web + + + + org.springframework.boot + spring-boot-configuration-processor + true + + + + org.springframework.boot + spring-boot-starter-test + test + + + + org.springframework.boot + spring-boot-starter-validation + + + + org.springframework.boot + spring-boot-starter-jdbc + 3.4.1 + + + + org.postgresql + postgresql + runtime + + + + com.h2database + h2 + runtime + + + + ru.practicum + stat-dto + 0.0.1-SNAPSHOT + compile + + + + + + 21 + 21 + UTF-8 + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + + \ No newline at end of file diff --git a/stats-server/stat-svc/src/main/java/ru/practicum/statsvc/StatSvcApp.java b/stats-server/stat-svc/src/main/java/ru/practicum/statsvc/StatSvcApp.java new file mode 100644 index 0000000..4d521b8 --- /dev/null +++ b/stats-server/stat-svc/src/main/java/ru/practicum/statsvc/StatSvcApp.java @@ -0,0 +1,11 @@ +package ru.practicum.statsvc; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class StatSvcApp { + public static void main(String[] args) { + SpringApplication.run(StatSvcApp.class, args); + } +} diff --git a/stats-server/stat-svc/src/main/java/ru/practicum/statsvc/controller/ErrorAdvisor.java b/stats-server/stat-svc/src/main/java/ru/practicum/statsvc/controller/ErrorAdvisor.java new file mode 100644 index 0000000..b8fbc50 --- /dev/null +++ b/stats-server/stat-svc/src/main/java/ru/practicum/statsvc/controller/ErrorAdvisor.java @@ -0,0 +1,30 @@ +package ru.practicum.statsvc.controller; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestControllerAdvice; +import ru.practicum.statdto.ErrorMessage; +import ru.practicum.statsvc.exception.InternalServerException; +import ru.practicum.statsvc.exception.ValidationException; + +@Slf4j +@RestControllerAdvice +public class ErrorAdvisor { + + @ExceptionHandler(ValidationException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public ErrorMessage onValidationException(ValidationException exception) { + log.error("400 {}.", exception.getMessage()); + return new ErrorMessage(exception.getMessage()); + } + + @ExceptionHandler + @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) + public ErrorMessage onInternalException(final InternalServerException e) { + log.error("500 {}", e.getMessage()); + return new ErrorMessage(e.getMessage()); + } + +} diff --git a/stats-server/stat-svc/src/main/java/ru/practicum/statsvc/controller/StatController.java b/stats-server/stat-svc/src/main/java/ru/practicum/statsvc/controller/StatController.java new file mode 100644 index 0000000..98bb153 --- /dev/null +++ b/stats-server/stat-svc/src/main/java/ru/practicum/statsvc/controller/StatController.java @@ -0,0 +1,39 @@ +package ru.practicum.statsvc.controller; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.*; +import ru.practicum.statdto.HitDto; +import ru.practicum.statdto.StatsDto; +import ru.practicum.statsvc.service.StatService; + +import java.util.List; + +@Slf4j +@RequiredArgsConstructor +@RestController +@RequestMapping +public class StatController { + + private final StatService statService; + + @PostMapping("/hit") + @ResponseStatus(HttpStatus.CREATED) + public void hit(@RequestBody HitDto dto) { + log.info("Поступила информация о посещении : " + dto.toString()); + statService.addHit(dto); + } + + @GetMapping("/stats") + @ResponseStatus(HttpStatus.OK) + public List getStats( + @RequestParam(required = false) String start, + @RequestParam(required = false) String end, + @RequestParam(required = false) List uris, + @RequestParam(defaultValue = "false") Boolean unique, + @RequestParam(defaultValue = "10") Integer size) { + log.info("Запрашивается информация о посещении эндпоинта {} с {} до {}.", uris, start, end); + return statService.getStats(start, end, uris, unique, size); + } +} diff --git a/stats-server/stat-svc/src/main/java/ru/practicum/statsvc/exception/InternalServerException.java b/stats-server/stat-svc/src/main/java/ru/practicum/statsvc/exception/InternalServerException.java new file mode 100644 index 0000000..685725d --- /dev/null +++ b/stats-server/stat-svc/src/main/java/ru/practicum/statsvc/exception/InternalServerException.java @@ -0,0 +1,7 @@ +package ru.practicum.statsvc.exception; + +public class InternalServerException extends RuntimeException { + public InternalServerException(String message) { + super(message); + } +} diff --git a/stats-server/stat-svc/src/main/java/ru/practicum/statsvc/exception/ValidationException.java b/stats-server/stat-svc/src/main/java/ru/practicum/statsvc/exception/ValidationException.java new file mode 100644 index 0000000..dba8ec4 --- /dev/null +++ b/stats-server/stat-svc/src/main/java/ru/practicum/statsvc/exception/ValidationException.java @@ -0,0 +1,7 @@ +package ru.practicum.statsvc.exception; + +public class ValidationException extends RuntimeException { + public ValidationException(String message) { + super(message); + } +} diff --git a/stats-server/stat-svc/src/main/java/ru/practicum/statsvc/mapper/EndpointMapper.java b/stats-server/stat-svc/src/main/java/ru/practicum/statsvc/mapper/EndpointMapper.java new file mode 100644 index 0000000..83cc122 --- /dev/null +++ b/stats-server/stat-svc/src/main/java/ru/practicum/statsvc/mapper/EndpointMapper.java @@ -0,0 +1,18 @@ +package ru.practicum.statsvc.mapper; + +import ru.practicum.statdto.HitDto; +import ru.practicum.statsvc.model.EndpointHit; + +public class EndpointMapper { + private EndpointMapper() { + } + + public static EndpointHit toEndpointHit(HitDto dto) { + EndpointHit endpoint = new EndpointHit(); + endpoint.setApp(dto.getApp()); + endpoint.setUri(dto.getUri()); + endpoint.setIp(dto.getIp()); + endpoint.setTimestamp(dto.getTimestamp()); + return endpoint; + } +} diff --git a/stats-server/stat-svc/src/main/java/ru/practicum/statsvc/mapper/ViewStatsMapper.java b/stats-server/stat-svc/src/main/java/ru/practicum/statsvc/mapper/ViewStatsMapper.java new file mode 100644 index 0000000..5cf4a1a --- /dev/null +++ b/stats-server/stat-svc/src/main/java/ru/practicum/statsvc/mapper/ViewStatsMapper.java @@ -0,0 +1,18 @@ +package ru.practicum.statsvc.mapper; + +import ru.practicum.statdto.StatsDto; +import ru.practicum.statsvc.model.ViewStats; + +public class ViewStatsMapper { + private ViewStatsMapper() { + } + + public static StatsDto toDto(ViewStats viewStats) { + StatsDto statsDto = new StatsDto( + viewStats.getApp(), + viewStats.getUri(), + viewStats.getHits() + ); + return statsDto; + } +} diff --git a/stats-server/stat-svc/src/main/java/ru/practicum/statsvc/mapper/ViewStatsRowMapper.java b/stats-server/stat-svc/src/main/java/ru/practicum/statsvc/mapper/ViewStatsRowMapper.java new file mode 100644 index 0000000..6d27e13 --- /dev/null +++ b/stats-server/stat-svc/src/main/java/ru/practicum/statsvc/mapper/ViewStatsRowMapper.java @@ -0,0 +1,18 @@ +package ru.practicum.statsvc.mapper; + +import org.springframework.jdbc.core.RowMapper; +import ru.practicum.statsvc.model.ViewStats; + +import java.sql.ResultSet; +import java.sql.SQLException; + +public class ViewStatsRowMapper implements RowMapper { + @Override + public ViewStats mapRow(ResultSet resultSet, int rowNum) throws SQLException { + ViewStats viewStats = new ViewStats(); + viewStats.setApp(resultSet.getString("app")); + viewStats.setUri(resultSet.getString("uri")); + viewStats.setHits(resultSet.getInt("hits")); + return viewStats; + } +} \ No newline at end of file diff --git a/stats-server/stat-svc/src/main/java/ru/practicum/statsvc/model/EndpointHit.java b/stats-server/stat-svc/src/main/java/ru/practicum/statsvc/model/EndpointHit.java new file mode 100644 index 0000000..35f8e8e --- /dev/null +++ b/stats-server/stat-svc/src/main/java/ru/practicum/statsvc/model/EndpointHit.java @@ -0,0 +1,20 @@ +package ru.practicum.statsvc.model; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +import java.time.LocalDateTime; + +@Setter +@Getter +@AllArgsConstructor +@NoArgsConstructor +public class EndpointHit { + private Integer id; + private String app; + private String uri; + private String ip; + private LocalDateTime timestamp; +} diff --git a/stats-server/stat-svc/src/main/java/ru/practicum/statsvc/model/ViewStats.java b/stats-server/stat-svc/src/main/java/ru/practicum/statsvc/model/ViewStats.java new file mode 100644 index 0000000..343b3eb --- /dev/null +++ b/stats-server/stat-svc/src/main/java/ru/practicum/statsvc/model/ViewStats.java @@ -0,0 +1,16 @@ +package ru.practicum.statsvc.model; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Setter +@Getter +@AllArgsConstructor +@NoArgsConstructor +public class ViewStats { + String app; + String uri; + Integer hits; +} diff --git a/stats-server/stat-svc/src/main/java/ru/practicum/statsvc/repository/StatDbStorage.java b/stats-server/stat-svc/src/main/java/ru/practicum/statsvc/repository/StatDbStorage.java new file mode 100644 index 0000000..e6ff0be --- /dev/null +++ b/stats-server/stat-svc/src/main/java/ru/practicum/statsvc/repository/StatDbStorage.java @@ -0,0 +1,105 @@ +package ru.practicum.statsvc.repository; + +import org.springframework.dao.DataAccessException; +import org.springframework.dao.EmptyResultDataAccessException; +import org.springframework.jdbc.core.namedparam.MapSqlParameterSource; +import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; +import org.springframework.jdbc.support.GeneratedKeyHolder; +import org.springframework.stereotype.Repository; +import ru.practicum.statsvc.exception.InternalServerException; +import ru.practicum.statsvc.mapper.ViewStatsRowMapper; +import ru.practicum.statsvc.model.EndpointHit; +import ru.practicum.statsvc.model.ViewStats; + +import java.sql.Types; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.List; + +@Repository +public class StatDbStorage implements StatStorage { + private static final DateTimeFormatter DATA_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); + private static final String SQL_INSERT_HIT = """ + INSERT INTO endpointhits (app, uri, ip, timestamp) + VALUES ( :app, :uri, :ip, :timestamp) + """; + + private final NamedParameterJdbcTemplate jdbc; + + public StatDbStorage(NamedParameterJdbcTemplate jdbc) { + this.jdbc = jdbc; + } + + @Override + public EndpointHit addHit(EndpointHit hit) { + GeneratedKeyHolder generatedKeyHolder = new GeneratedKeyHolder(); + try { + jdbc.update(SQL_INSERT_HIT, + new MapSqlParameterSource() + .addValue("app", hit.getApp()) + .addValue("uri", hit.getUri()) + .addValue("ip", hit.getIp()) + .addValue("timestamp", hit.getTimestamp().format(DATA_TIME_FORMATTER), Types.TIMESTAMP), + generatedKeyHolder, new String[]{"id"} + ); + } catch (DataAccessException e) { + throw new InternalServerException("Ошибка при сохранении в базу данных. " + + e.getMessage()); + } + // получаем идентификатор + final Integer hitId = generatedKeyHolder.getKey().intValue(); + hit.setId(hitId); + + return hit; + } + + @Override + public List getViewStats(LocalDateTime start, LocalDateTime end, List uris, Boolean unique, Integer size) { + StringBuilder sql = new StringBuilder(); + sql.append("SELECT app, uri, count(ip) as hits FROM"); + if (unique) { + sql.append(" (SELECT DISTINCT ON (ip) app, uri, ip, timestamp FROM endpointhits)"); + } else { + sql.append(" endpointhits"); + } + MapSqlParameterSource parameters = new MapSqlParameterSource(); + Boolean whereFlag = false; + + if (uris != null && !uris.isEmpty()) { + sql.append(" WHERE uri IN (:uris)"); + parameters.addValue("uris", uris); + whereFlag = true; + } + + if (start != null) { + if (whereFlag) { + sql.append(" AND timestamp >= :start"); + } else { + sql.append(" WHERE timestamp >= :start"); + whereFlag = true; + } + parameters.addValue("start", start); + } + if (end != null) { + if (whereFlag) { + sql.append(" AND timestamp < :end"); + } else { + sql.append(" WHERE timestamp < :end"); + } + parameters.addValue("end", end); + } + sql.append(" GROUP BY uri, app ORDER BY hits DESC"); + if (size != null) { + parameters.addValue("size", size); + sql.append(" LIMIT :size"); + } + try { + List viewStatsList = jdbc.query(sql.toString(), + parameters, + new ViewStatsRowMapper()); + return viewStatsList; + } catch (EmptyResultDataAccessException ignored) { + return List.of(); + } + } +} diff --git a/stats-server/stat-svc/src/main/java/ru/practicum/statsvc/repository/StatStorage.java b/stats-server/stat-svc/src/main/java/ru/practicum/statsvc/repository/StatStorage.java new file mode 100644 index 0000000..573ee32 --- /dev/null +++ b/stats-server/stat-svc/src/main/java/ru/practicum/statsvc/repository/StatStorage.java @@ -0,0 +1,13 @@ +package ru.practicum.statsvc.repository; + +import ru.practicum.statsvc.model.EndpointHit; +import ru.practicum.statsvc.model.ViewStats; + +import java.time.LocalDateTime; +import java.util.List; + +public interface StatStorage { + EndpointHit addHit(EndpointHit hit); + + List getViewStats(LocalDateTime start, LocalDateTime end, List uris, Boolean unique, Integer size); +} diff --git a/stats-server/stat-svc/src/main/java/ru/practicum/statsvc/service/StatService.java b/stats-server/stat-svc/src/main/java/ru/practicum/statsvc/service/StatService.java new file mode 100644 index 0000000..cbc6fa8 --- /dev/null +++ b/stats-server/stat-svc/src/main/java/ru/practicum/statsvc/service/StatService.java @@ -0,0 +1,13 @@ +package ru.practicum.statsvc.service; + +import ru.practicum.statdto.HitDto; +import ru.practicum.statdto.StatsDto; + +import java.util.List; + +public interface StatService { + + public void addHit(HitDto hitDto); + + public List getStats(String startTxt, String endTxt, List uris, Boolean unique, Integer size); +} diff --git a/stats-server/stat-svc/src/main/java/ru/practicum/statsvc/service/StatServiceImpl.java b/stats-server/stat-svc/src/main/java/ru/practicum/statsvc/service/StatServiceImpl.java new file mode 100644 index 0000000..dbb381e --- /dev/null +++ b/stats-server/stat-svc/src/main/java/ru/practicum/statsvc/service/StatServiceImpl.java @@ -0,0 +1,59 @@ +package ru.practicum.statsvc.service; + +import org.springframework.stereotype.Service; +import ru.practicum.statdto.HitDto; +import ru.practicum.statdto.StatsDto; +import ru.practicum.statsvc.exception.ValidationException; +import ru.practicum.statsvc.mapper.EndpointMapper; +import ru.practicum.statsvc.mapper.ViewStatsMapper; +import ru.practicum.statsvc.repository.StatDbStorage; + +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; +import java.util.List; + +@Service +public class StatServiceImpl implements StatService { + private static final DateTimeFormatter DATA_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); + private final StatDbStorage storage; + + public StatServiceImpl(StatDbStorage storage) { + this.storage = storage; + } + + @Override + public void addHit(HitDto hitDto) { + storage.addHit(EndpointMapper.toEndpointHit(hitDto)); + } + + @Override + public List getStats(String startTxt, + String endTxt, + List uris, + Boolean unique, + Integer size) { + LocalDateTime start = null; + LocalDateTime end = null; + try { + if (startTxt != null && !startTxt.isEmpty()) { + start = LocalDateTime.parse(startTxt, DATA_TIME_FORMATTER); + } + if (endTxt != null && !endTxt.isEmpty()) { + end = LocalDateTime.parse(endTxt, DATA_TIME_FORMATTER); + } + } catch (DateTimeParseException e) { + throw new ValidationException("Некорректный формат времени. " + e.getMessage()); + } + + if (start != null && end != null) { + if (start.isAfter(end)) { + throw new ValidationException("Указан недопустимый промежуток времени"); + } + } + return storage.getViewStats(start, end, uris, unique, size) + .stream() + .map(ViewStatsMapper::toDto) + .toList(); + } +} diff --git a/stats-server/stat-svc/src/main/resources/application.properties b/stats-server/stat-svc/src/main/resources/application.properties new file mode 100644 index 0000000..a69735a --- /dev/null +++ b/stats-server/stat-svc/src/main/resources/application.properties @@ -0,0 +1,14 @@ +server.port=9090 +spring.sql.init.mode=always +spring.jackson.date-format=yyyy-MM-dd HH:mm:ss + +spring.datasource.driverClassName=org.postgresql.Driver +spring.datasource.url=jdbc:postgresql://192.168.0.102:5432/statdb +spring.datasource.username=statdb +spring.datasource.password=statdb + +#spring.datasource.driverClassName=org.h2.Driver +#spring.datasource.url=jdbc:h2:mem:statdb +#spring.datasource.username=statdb +#spring.datasource.password=statdb + diff --git a/stats-server/stat-svc/src/main/resources/schema.sql b/stats-server/stat-svc/src/main/resources/schema.sql new file mode 100644 index 0000000..431f17f --- /dev/null +++ b/stats-server/stat-svc/src/main/resources/schema.sql @@ -0,0 +1,8 @@ +CREATE TABLE IF NOT EXISTS endpointhits +( + id INTEGER GENERATED ALWAYS AS IDENTITY PRIMARY KEY, + app VARCHAR(128), + uri VARCHAR(128), + ip VARCHAR(128), + timestamp TIMESTAMP WITHOUT TIME ZONE +);