diff --git a/borschevskaya_anna_lab_3/README.md b/borschevskaya_anna_lab_3/README.md new file mode 100644 index 0000000..0aabe86 --- /dev/null +++ b/borschevskaya_anna_lab_3/README.md @@ -0,0 +1,58 @@ +# Отчет. Лабораторная работа 3 + +## Описание +В рамках лабораторной работы № 3 были реализованы два сервиса (Java + Spring), осуществляющие CRUD-операции над сущностями. + +Модель данных следующая: + +Сущность "Компания" (сервис company) +- идентификатор компании +- название +- адрес + +Сущность "Вакансия" (сервис vacancy) +- идентификатор вакансии +- название +- описание +- нижняя граница зарплаты +- верхняя граница зарплаты +- идентификатор компании + +Компания с вакансией связана как "один ко многим". + +Каждый из сервисов имеет API с пятью эндпоинтами. При этом в сервисе vacancy при запросе вакансии по id происходит +дополнительный запрос в сервис company для получения информации по компании, связанной с вакансией. +Происходит это взаимодействие с помощью библиотеки OpenFeign, которая "под капотом" использует HttpClient. + +В качестве хранилища данных использовалась СУБД Postgres. У каждого сервиса своя база данных в поднятой СУБД. +Для создания схемы БД была использована библиотека Flyway, которая применила написанные миграции при старте приложения. + +Запросы к сервисам проксирует шлюз на основе Nginx. Для этого перед запуском nginx был описан конфигурационный файл nginx.conf, +в котором описан прослушиваемый порт и название сервера (в блоке server), маршруты до микросервисов и параметры проксирования (в блоке location). + +Таким образом, с помощью Docker Compose были подняты сервисы: +- company +- vacancy +- postgres +- nginx +## Как запустить +Для того, чтобы запустить сервисы, необходимо выполнить следующие действия: +1. Установить и запустить Docker Engine или Docker Desktop +2. Через консоль перейти в папку, в которой расположен файл docker-compose.yml +3. Выполнить команду: +``` +docker compose up --build +``` +В случае успешного запуска всех контейнеров в консоли будет выведено следующее сообщение: +``` +[+] Running 5/5 + ✔ Network borschevskaya_anna_lab_3_default Created 0.0s + ✔ Container postgres Started 0.6s + ✔ Container vacancy Started 1.1s + ✔ Container company Started 0.9s + ✔ Container borschevskaya_anna_lab_3-nginx-1 Started +``` +Далее можно осуществлять запросы к сервисам по адресу http://localhost/{location}, где часть пути location меняется в зависимости от сервиса и запроса. +## Видео-отчет +Работоспособность лабораторной работы можно оценить в следующем [видео](https://disk.yandex.ru/i/KPNBfnlcgl1auw). +Демонстрация взаимодействия с системой производится с применением сервиса Postman. \ No newline at end of file diff --git a/borschevskaya_anna_lab_3/company-service/.gitignore b/borschevskaya_anna_lab_3/company-service/.gitignore new file mode 100644 index 0000000..5ff6309 --- /dev/null +++ b/borschevskaya_anna_lab_3/company-service/.gitignore @@ -0,0 +1,38 @@ +target/ +!.mvn/wrapper/maven-wrapper.jar +!**/src/main/**/target/ +!**/src/test/**/target/ + +### IntelliJ IDEA ### +.idea/modules.xml +.idea/jarRepositories.xml +.idea/compiler.xml +.idea/libraries/ +*.iws +*.iml +*.ipr + +### Eclipse ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ +!**/src/main/**/build/ +!**/src/test/**/build/ + +### VS Code ### +.vscode/ + +### Mac OS ### +.DS_Store \ No newline at end of file diff --git a/borschevskaya_anna_lab_3/company-service/Dockerfile b/borschevskaya_anna_lab_3/company-service/Dockerfile new file mode 100644 index 0000000..cc673eb --- /dev/null +++ b/borschevskaya_anna_lab_3/company-service/Dockerfile @@ -0,0 +1,21 @@ +# Используем образ Maven для сборки +FROM maven:3.8-eclipse-temurin-21-alpine AS build + +# Устанавливаем рабочую директорию +WORKDIR /app + +# Копируем остальные исходные файлы +COPY pom.xml . +COPY src src + +# Собираем весь проект +RUN mvn clean package -DskipTests + +# Используем официальный образ JDK для запуска собранного jar-файла +FROM eclipse-temurin:21-jdk-alpine + +# Копируем jar-файл из предыдущего этапа +COPY --from=build /app/target/*.jar /app.jar + +# Указываем команду для запуска приложения +CMD ["java", "-jar", "app.jar"] diff --git a/borschevskaya_anna_lab_3/company-service/pom.xml b/borschevskaya_anna_lab_3/company-service/pom.xml new file mode 100644 index 0000000..96bd4a8 --- /dev/null +++ b/borschevskaya_anna_lab_3/company-service/pom.xml @@ -0,0 +1,66 @@ + + + 4.0.0 + + ru.somecompany + company-service + 1.0-SNAPSHOT + + + org.springframework.boot + spring-boot-starter-parent + 3.2.3 + + + + + 21 + 21 + UTF-8 + + + + + org.springframework.boot + spring-boot-starter-data-jpa + + + org.springframework.boot + spring-boot-starter-web + + + org.flywaydb + flyway-core + + + org.postgresql + postgresql + + + org.projectlombok + lombok + 1.18.30 + + + io.swagger.core.v3 + swagger-annotations + 2.2.1 + + + org.springdoc + springdoc-openapi-starter-webmvc-ui + 2.3.0 + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + \ No newline at end of file diff --git a/borschevskaya_anna_lab_3/company-service/src/main/java/ru/somecompany/CompanyApplication.java b/borschevskaya_anna_lab_3/company-service/src/main/java/ru/somecompany/CompanyApplication.java new file mode 100644 index 0000000..1d4b094 --- /dev/null +++ b/borschevskaya_anna_lab_3/company-service/src/main/java/ru/somecompany/CompanyApplication.java @@ -0,0 +1,12 @@ +package ru.somecompany; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class CompanyApplication { + + public static void main(String[] args) { + SpringApplication.run(CompanyApplication.class, args); + } +} \ No newline at end of file diff --git a/borschevskaya_anna_lab_3/company-service/src/main/java/ru/somecompany/controller/CompanyController.java b/borschevskaya_anna_lab_3/company-service/src/main/java/ru/somecompany/controller/CompanyController.java new file mode 100644 index 0000000..addbe5d --- /dev/null +++ b/borschevskaya_anna_lab_3/company-service/src/main/java/ru/somecompany/controller/CompanyController.java @@ -0,0 +1,44 @@ +package ru.somecompany.controller; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.constraints.NotNull; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import ru.somecompany.domain.CompanyEntity; +import ru.somecompany.domain.CreateCompanyRequest; + +import java.util.List; +import java.util.UUID; + + +@Validated +@Tag(name = "company", description = "API для управления компаниями") +public interface CompanyController { + + @Operation(summary = "Получение всех компаний") + @GetMapping(value = "/api/v1/company") + List getCompanies(); + + @Operation(summary = "Создание компании") + @PostMapping(value = "/api/v1/company") + CompanyEntity createCompany(@RequestBody @NotNull CreateCompanyRequest companyRequest); + + @Operation(summary = "Получение информации о компании по id") + @GetMapping(value = "/api/v1/company/{companyId}") + CompanyEntity getCompany(@PathVariable UUID companyId); + + @Operation(summary = "Редактирование компании") + @PutMapping(value = "/api/v1/company/{companyId}") + CompanyEntity updateCompany(@PathVariable UUID companyId, + @RequestBody @NotNull CreateCompanyRequest companyRequest); + + @Operation(summary = "Удалении компании по id") + @DeleteMapping(value = "/api/v1/company/{companyId}") + void deleteCompany(@PathVariable UUID companyId); +} diff --git a/borschevskaya_anna_lab_3/company-service/src/main/java/ru/somecompany/controller/CompanyControllerImpl.java b/borschevskaya_anna_lab_3/company-service/src/main/java/ru/somecompany/controller/CompanyControllerImpl.java new file mode 100644 index 0000000..a15ca21 --- /dev/null +++ b/borschevskaya_anna_lab_3/company-service/src/main/java/ru/somecompany/controller/CompanyControllerImpl.java @@ -0,0 +1,42 @@ +package ru.somecompany.controller; + +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.RestController; +import ru.somecompany.domain.CompanyEntity; +import ru.somecompany.domain.CreateCompanyRequest; +import ru.somecompany.service.CompanyService; + +import java.util.List; +import java.util.UUID; + +@RestController +@RequiredArgsConstructor +public class CompanyControllerImpl implements CompanyController { + + private final CompanyService companyService; + + @Override + public List getCompanies() { + return companyService.getCompanies(); + } + + @Override + public CompanyEntity createCompany(CreateCompanyRequest companyRequest) { + return companyService.createCompany(companyRequest); + } + + @Override + public CompanyEntity getCompany(UUID companyId) { + return companyService.getCompany(companyId); + } + + @Override + public CompanyEntity updateCompany(UUID companyId, CreateCompanyRequest companyRequest) { + return companyService.updateCompany(companyId, companyRequest); + } + + @Override + public void deleteCompany(UUID companyId) { + companyService.deleteCompany(companyId); + } +} diff --git a/borschevskaya_anna_lab_3/company-service/src/main/java/ru/somecompany/domain/CompanyEntity.java b/borschevskaya_anna_lab_3/company-service/src/main/java/ru/somecompany/domain/CompanyEntity.java new file mode 100644 index 0000000..5097043 --- /dev/null +++ b/borschevskaya_anna_lab_3/company-service/src/main/java/ru/somecompany/domain/CompanyEntity.java @@ -0,0 +1,32 @@ +package ru.somecompany.domain; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.UUID; + +@Data +@Entity +@Builder +@Table(name = "company") +@AllArgsConstructor +@NoArgsConstructor +public class CompanyEntity { + + @Id + @GeneratedValue + private UUID id; + + @Column(name = "name", unique = true, nullable = false) + private String name; + + @Column(name = "address", unique = true, nullable = false) + private String address; +} diff --git a/borschevskaya_anna_lab_3/company-service/src/main/java/ru/somecompany/domain/CreateCompanyRequest.java b/borschevskaya_anna_lab_3/company-service/src/main/java/ru/somecompany/domain/CreateCompanyRequest.java new file mode 100644 index 0000000..8c0a665 --- /dev/null +++ b/borschevskaya_anna_lab_3/company-service/src/main/java/ru/somecompany/domain/CreateCompanyRequest.java @@ -0,0 +1,13 @@ +package ru.somecompany.domain; + +import lombok.AllArgsConstructor; +import lombok.Data; + +@Data +@AllArgsConstructor +public class CreateCompanyRequest { + + private String name; + + private String address; +} diff --git a/borschevskaya_anna_lab_3/company-service/src/main/java/ru/somecompany/exception/AbstractWebExceptionHandler.java b/borschevskaya_anna_lab_3/company-service/src/main/java/ru/somecompany/exception/AbstractWebExceptionHandler.java new file mode 100644 index 0000000..fc34e6f --- /dev/null +++ b/borschevskaya_anna_lab_3/company-service/src/main/java/ru/somecompany/exception/AbstractWebExceptionHandler.java @@ -0,0 +1,41 @@ +package ru.somecompany.exception; + +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; + +import java.time.LocalDateTime; + +import static org.springframework.http.HttpStatus.NOT_FOUND; + +@Slf4j +public abstract class AbstractWebExceptionHandler { + + public static ResponseEntity buildResponse(ErrorCode errorCode, HttpStatus status, String msg, + boolean details, Throwable ex) { + return buildResponse(errorCode.name(), status, msg, details, ex, LocalDateTime.now()); + } + + public static ResponseEntity buildResponse(String errorCode, HttpStatus status, String msg, + boolean details, Throwable ex, LocalDateTime timestamp) { + if (details) { + log.error(msg, ex); // with stack-trace + } else { + var message = StringUtils.equals(msg, ex.getMessage()) ? msg : msg + ": " + ex.getMessage(); + if (status == NOT_FOUND) { + log.warn(message); + } else { + log.error(message); + } + } + + return ResponseEntity.status(status.value()) + .body(ResponseError.builder() + .code(errorCode) + .message(msg) + .timestamp(timestamp) + .status(status.value()) + .build()); + } +} diff --git a/borschevskaya_anna_lab_3/company-service/src/main/java/ru/somecompany/exception/BasicError.java b/borschevskaya_anna_lab_3/company-service/src/main/java/ru/somecompany/exception/BasicError.java new file mode 100644 index 0000000..baca00a --- /dev/null +++ b/borschevskaya_anna_lab_3/company-service/src/main/java/ru/somecompany/exception/BasicError.java @@ -0,0 +1,5 @@ +package ru.somecompany.exception; + +public enum BasicError implements ErrorCode { + NOT_FOUND +} diff --git a/borschevskaya_anna_lab_3/company-service/src/main/java/ru/somecompany/exception/CompanyAppRuntimeException.java b/borschevskaya_anna_lab_3/company-service/src/main/java/ru/somecompany/exception/CompanyAppRuntimeException.java new file mode 100644 index 0000000..5148716 --- /dev/null +++ b/borschevskaya_anna_lab_3/company-service/src/main/java/ru/somecompany/exception/CompanyAppRuntimeException.java @@ -0,0 +1,26 @@ +package ru.somecompany.exception; + +import lombok.Getter; +import org.springframework.http.HttpStatus; + +@Getter +public class CompanyAppRuntimeException extends RuntimeException { + private final ErrorCode errorCode; + private final HttpStatus status; + private final boolean details; + + public CompanyAppRuntimeException(ErrorCode errorCode, HttpStatus status, String message) { + this(errorCode, status, message, null); + } + + public CompanyAppRuntimeException(ErrorCode errorCode, HttpStatus status, String message, Throwable error) { + this(errorCode, status, message, true, error); + } + + public CompanyAppRuntimeException(ErrorCode errorCode, HttpStatus status, String message, boolean details, Throwable error) { + super(message, error); + this.errorCode = errorCode; + this.status = status; + this.details = details; + } +} diff --git a/borschevskaya_anna_lab_3/company-service/src/main/java/ru/somecompany/exception/ErrorCode.java b/borschevskaya_anna_lab_3/company-service/src/main/java/ru/somecompany/exception/ErrorCode.java new file mode 100644 index 0000000..ed73439 --- /dev/null +++ b/borschevskaya_anna_lab_3/company-service/src/main/java/ru/somecompany/exception/ErrorCode.java @@ -0,0 +1,9 @@ +package ru.somecompany.exception; + +import java.io.Serializable; + +public interface ErrorCode extends Serializable { + + String name(); + +} diff --git a/borschevskaya_anna_lab_3/company-service/src/main/java/ru/somecompany/exception/ResourceNotFoundException.java b/borschevskaya_anna_lab_3/company-service/src/main/java/ru/somecompany/exception/ResourceNotFoundException.java new file mode 100644 index 0000000..7ecd7c0 --- /dev/null +++ b/borschevskaya_anna_lab_3/company-service/src/main/java/ru/somecompany/exception/ResourceNotFoundException.java @@ -0,0 +1,28 @@ +package ru.somecompany.exception; + +import org.springframework.http.HttpStatus; + +import static ru.somecompany.exception.BasicError.NOT_FOUND; + +public class ResourceNotFoundException extends CompanyAppRuntimeException { + + public ResourceNotFoundException(ErrorCode errorCode, String message) { + this(errorCode, message, null); + } + + public ResourceNotFoundException(ErrorCode errorCode, String message, Throwable error) { + super(errorCode, HttpStatus.NOT_FOUND, message, error); + } + + public static ResourceNotFoundException notFound(String message, Object... args) { + return new ResourceNotFoundException(NOT_FOUND, String.format(message, args), null); + } + + public static ResourceNotFoundException notFound() { + return new ResourceNotFoundException(NOT_FOUND, "Запрашиваемые данные не найдены", null); + } + + public static ResourceNotFoundException notFound(Throwable error) { + return new ResourceNotFoundException(NOT_FOUND, error.getMessage(), error); + } +} \ No newline at end of file diff --git a/borschevskaya_anna_lab_3/company-service/src/main/java/ru/somecompany/exception/ResponseError.java b/borschevskaya_anna_lab_3/company-service/src/main/java/ru/somecompany/exception/ResponseError.java new file mode 100644 index 0000000..4ae118d --- /dev/null +++ b/borschevskaya_anna_lab_3/company-service/src/main/java/ru/somecompany/exception/ResponseError.java @@ -0,0 +1,24 @@ +package ru.somecompany.exception; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; + +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class ResponseError { + @JsonProperty(required = true) + private LocalDateTime timestamp; + @JsonProperty(required = true) + private Integer status; + @JsonProperty(required = true) + private String message; + @JsonProperty(required = true) + private String code; +} diff --git a/borschevskaya_anna_lab_3/company-service/src/main/java/ru/somecompany/exception/WebExceptionHandler.java b/borschevskaya_anna_lab_3/company-service/src/main/java/ru/somecompany/exception/WebExceptionHandler.java new file mode 100644 index 0000000..78fa51f --- /dev/null +++ b/borschevskaya_anna_lab_3/company-service/src/main/java/ru/somecompany/exception/WebExceptionHandler.java @@ -0,0 +1,22 @@ +package ru.somecompany.exception; + +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +@Slf4j +@Getter +@RestControllerAdvice +@RequiredArgsConstructor +public class WebExceptionHandler extends AbstractWebExceptionHandler { + private final ObjectMapper objectMapper; + + @ExceptionHandler(CompanyAppRuntimeException.class) + public ResponseEntity handleException(CompanyAppRuntimeException ex) { + return buildResponse(ex.getErrorCode(), ex.getStatus(), ex.getMessage(), ex.isDetails(), ex); + } +} diff --git a/borschevskaya_anna_lab_3/company-service/src/main/java/ru/somecompany/repository/CompanyRepository.java b/borschevskaya_anna_lab_3/company-service/src/main/java/ru/somecompany/repository/CompanyRepository.java new file mode 100644 index 0000000..2cce0d8 --- /dev/null +++ b/borschevskaya_anna_lab_3/company-service/src/main/java/ru/somecompany/repository/CompanyRepository.java @@ -0,0 +1,9 @@ +package ru.somecompany.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import ru.somecompany.domain.CompanyEntity; + +import java.util.UUID; + +public interface CompanyRepository extends JpaRepository { +} diff --git a/borschevskaya_anna_lab_3/company-service/src/main/java/ru/somecompany/service/CompanyService.java b/borschevskaya_anna_lab_3/company-service/src/main/java/ru/somecompany/service/CompanyService.java new file mode 100644 index 0000000..0c306e5 --- /dev/null +++ b/borschevskaya_anna_lab_3/company-service/src/main/java/ru/somecompany/service/CompanyService.java @@ -0,0 +1,20 @@ +package ru.somecompany.service; + +import ru.somecompany.domain.CompanyEntity; +import ru.somecompany.domain.CreateCompanyRequest; + +import java.util.List; +import java.util.UUID; + +public interface CompanyService { + + List getCompanies(); + + CompanyEntity createCompany(CreateCompanyRequest companyRequest); + + CompanyEntity updateCompany(UUID companyId, CreateCompanyRequest companyRequest); + + void deleteCompany(UUID companyId); + + CompanyEntity getCompany(UUID companyId); +} diff --git a/borschevskaya_anna_lab_3/company-service/src/main/java/ru/somecompany/service/CompanyServiceImpl.java b/borschevskaya_anna_lab_3/company-service/src/main/java/ru/somecompany/service/CompanyServiceImpl.java new file mode 100644 index 0000000..b33c2f4 --- /dev/null +++ b/borschevskaya_anna_lab_3/company-service/src/main/java/ru/somecompany/service/CompanyServiceImpl.java @@ -0,0 +1,57 @@ +package ru.somecompany.service; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import ru.somecompany.domain.CompanyEntity; +import ru.somecompany.domain.CreateCompanyRequest; +import ru.somecompany.repository.CompanyRepository; + +import java.util.List; +import java.util.UUID; + +import static ru.somecompany.exception.ResourceNotFoundException.notFound; + +@Service +@RequiredArgsConstructor +public class CompanyServiceImpl implements CompanyService { + + private final CompanyRepository companyRepository; + + @Override + public List getCompanies() { + return companyRepository.findAll(); + } + + @Override + public CompanyEntity createCompany(CreateCompanyRequest companyRequest) { + var entity = CompanyEntity.builder() + .name(companyRequest.getName()) + .address(companyRequest.getAddress()) + .build(); + return companyRepository.save(entity); + } + + @Override + public CompanyEntity updateCompany(UUID companyId, CreateCompanyRequest companyRequest) { + var company = getById(companyId); + company.setName(companyRequest.getName()); + company.setAddress(companyRequest.getAddress()); + return companyRepository.save(company); + } + + @Override + public void deleteCompany(UUID companyId) { + var company = getById(companyId); + companyRepository.delete(company); + } + + @Override + public CompanyEntity getCompany(UUID companyId) { + return getById(companyId); + } + + private CompanyEntity getById(UUID companyId) { + return companyRepository.findById(companyId) + .orElseThrow(() -> notFound("Компания не найдена")); + } +} diff --git a/borschevskaya_anna_lab_3/company-service/src/main/resources/application.yml b/borschevskaya_anna_lab_3/company-service/src/main/resources/application.yml new file mode 100644 index 0000000..1580336 --- /dev/null +++ b/borschevskaya_anna_lab_3/company-service/src/main/resources/application.yml @@ -0,0 +1,28 @@ +server: + port: ${SERVER_PORT:8080} +spring: + application: + name: company-app + jpa: + database: POSTGRESQL + open-in-view: false + show-sql: false + hibernate: + ddl-auto: none + properties: + hibernate: + dialect: org.hibernate.dialect.PostgreSQLDialect + datasource: + url: ${DB_URL:jdbc:postgresql://postgres:5433/company} + username: ${DB_USERNAME:postgres} + password: ${DB_PASSWORD:postgres} + driverClassName: org.postgresql.Driver + type: com.zaxxer.hikari.HikariDataSource + hikari: + maximum-pool-size: 15 + minimum-idle: 4 + idle-timeout: 180000 + max-lifetime: 599000 + flyway: + enabled: true + locations: db/migration/ \ No newline at end of file diff --git a/borschevskaya_anna_lab_3/company-service/src/main/resources/db/migration/V000001__init.sql b/borschevskaya_anna_lab_3/company-service/src/main/resources/db/migration/V000001__init.sql new file mode 100644 index 0000000..7704135 --- /dev/null +++ b/borschevskaya_anna_lab_3/company-service/src/main/resources/db/migration/V000001__init.sql @@ -0,0 +1,6 @@ +CREATE TABLE company ( + id UUID + CONSTRAINT company_pk PRIMARY KEY, + name VARCHAR(255) UNIQUE NOT NULL, + address VARCHAR(255) NOT NULL +); diff --git a/borschevskaya_anna_lab_3/docker-compose.yml b/borschevskaya_anna_lab_3/docker-compose.yml new file mode 100644 index 0000000..da5aa85 --- /dev/null +++ b/borschevskaya_anna_lab_3/docker-compose.yml @@ -0,0 +1,43 @@ +services: + postgres: + image: postgres:latest + container_name: postgres + environment: + POSTGRES_USERNAME: postgres + POSTGRES_PASSWORD: postgres + PGDATA: "/var/lib/postgresql/data/pgdata" + ports: + - "5432:5432" + volumes: + - ./postgres_data:/var/lib/postgresql/data/ + - ./init-database.sh:/docker-entrypoint-initdb.d/init-database.sh + company: + build: ./company-service + container_name: company + depends_on: + - postgres + environment: + SERVER_PORT: 8080 + DB_URL: jdbc:postgresql://postgres:5432/company + DB_USERNAME: postgres + DB_PASSWORD: postgres + vacancy: + build: ./vacancy-service + container_name: vacancy + depends_on: + - postgres + environment: + SERVER_PORT: 8080 + DB_URL: jdbc:postgresql://postgres:5432/vacancy + DB_USERNAME: postgres + DB_PASSWORD: postgres + COMPANY_URL: http://nginx/ + nginx: + image: nginx + depends_on: + - vacancy + - company + volumes: + - ./nginx/nginx.conf:/etc/nginx/conf.d/default.conf + ports: + - 80:80 \ No newline at end of file diff --git a/borschevskaya_anna_lab_3/init-database.sh b/borschevskaya_anna_lab_3/init-database.sh new file mode 100644 index 0000000..63e5d3a --- /dev/null +++ b/borschevskaya_anna_lab_3/init-database.sh @@ -0,0 +1,8 @@ +#!/bin/bash +set -e + +# Создаем БД +psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<-EOSQL + CREATE DATABASE company; + CREATE DATABASE vacancy; +EOSQL \ No newline at end of file diff --git a/borschevskaya_anna_lab_3/nginx/nginx.conf b/borschevskaya_anna_lab_3/nginx/nginx.conf new file mode 100644 index 0000000..4a27944 --- /dev/null +++ b/borschevskaya_anna_lab_3/nginx/nginx.conf @@ -0,0 +1,21 @@ +server { + listen 80; + listen [::]:80; + server_name localhost; + + location /api/v1/company { + proxy_pass http://company:8080; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + } + + location /api/v1/vacancy { + proxy_pass http://vacancy:8080; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + } +} \ No newline at end of file diff --git a/borschevskaya_anna_lab_3/vacancy-service/.gitignore b/borschevskaya_anna_lab_3/vacancy-service/.gitignore new file mode 100644 index 0000000..5ff6309 --- /dev/null +++ b/borschevskaya_anna_lab_3/vacancy-service/.gitignore @@ -0,0 +1,38 @@ +target/ +!.mvn/wrapper/maven-wrapper.jar +!**/src/main/**/target/ +!**/src/test/**/target/ + +### IntelliJ IDEA ### +.idea/modules.xml +.idea/jarRepositories.xml +.idea/compiler.xml +.idea/libraries/ +*.iws +*.iml +*.ipr + +### Eclipse ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ +!**/src/main/**/build/ +!**/src/test/**/build/ + +### VS Code ### +.vscode/ + +### Mac OS ### +.DS_Store \ No newline at end of file diff --git a/borschevskaya_anna_lab_3/vacancy-service/Dockerfile b/borschevskaya_anna_lab_3/vacancy-service/Dockerfile new file mode 100644 index 0000000..cc673eb --- /dev/null +++ b/borschevskaya_anna_lab_3/vacancy-service/Dockerfile @@ -0,0 +1,21 @@ +# Используем образ Maven для сборки +FROM maven:3.8-eclipse-temurin-21-alpine AS build + +# Устанавливаем рабочую директорию +WORKDIR /app + +# Копируем остальные исходные файлы +COPY pom.xml . +COPY src src + +# Собираем весь проект +RUN mvn clean package -DskipTests + +# Используем официальный образ JDK для запуска собранного jar-файла +FROM eclipse-temurin:21-jdk-alpine + +# Копируем jar-файл из предыдущего этапа +COPY --from=build /app/target/*.jar /app.jar + +# Указываем команду для запуска приложения +CMD ["java", "-jar", "app.jar"] diff --git a/borschevskaya_anna_lab_3/vacancy-service/pom.xml b/borschevskaya_anna_lab_3/vacancy-service/pom.xml new file mode 100644 index 0000000..e6374ce --- /dev/null +++ b/borschevskaya_anna_lab_3/vacancy-service/pom.xml @@ -0,0 +1,128 @@ + + + 4.0.0 + + ru.somecompany + vacancy-service + 1.0-SNAPSHOT + + + org.springframework.boot + spring-boot-starter-parent + 3.2.3 + + + + + 21 + 21 + UTF-8 + 1.5.5.Final + 0.2.0 + 2023.0.3 + + + + + + org.springframework.boot + spring-boot-starter-data-jpa + + + org.springframework.boot + spring-boot-starter-web + + + org.flywaydb + flyway-core + + + org.postgresql + postgresql + + + org.projectlombok + lombok + 1.18.30 + + + org.mapstruct + mapstruct + ${mapstruct.version} + + + + org.projectlombok + lombok-mapstruct-binding + ${lombok-mapstruct-binding.version} + + + org.springframework.cloud + spring-cloud-starter-openfeign + + + io.swagger.core.v3 + swagger-annotations + 2.2.1 + + + org.springdoc + springdoc-openapi-starter-webmvc-ui + 2.3.0 + + + + + + org.springframework.cloud + spring-cloud-dependencies + ${spring-cloud.version} + pom + import + + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + + + org.mapstruct + mapstruct-processor + ${mapstruct.version} + + + org.projectlombok + lombok + 1.18.30 + + + org.projectlombok + lombok-mapstruct-binding + ${lombok-mapstruct-binding.version} + + + + -Amapstruct.defaultComponentModel=spring + + + + + + + + + \ No newline at end of file diff --git a/borschevskaya_anna_lab_3/vacancy-service/src/main/java/ru/somecompany/VacancyApplication.java b/borschevskaya_anna_lab_3/vacancy-service/src/main/java/ru/somecompany/VacancyApplication.java new file mode 100644 index 0000000..5c71f93 --- /dev/null +++ b/borschevskaya_anna_lab_3/vacancy-service/src/main/java/ru/somecompany/VacancyApplication.java @@ -0,0 +1,14 @@ +package ru.somecompany; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.cloud.openfeign.EnableFeignClients; + +@SpringBootApplication +@EnableFeignClients +public class VacancyApplication { + + public static void main(String[] args) { + SpringApplication.run(VacancyApplication.class, args); + } +} \ No newline at end of file diff --git a/borschevskaya_anna_lab_3/vacancy-service/src/main/java/ru/somecompany/controller/VacancyController.java b/borschevskaya_anna_lab_3/vacancy-service/src/main/java/ru/somecompany/controller/VacancyController.java new file mode 100644 index 0000000..d260e55 --- /dev/null +++ b/borschevskaya_anna_lab_3/vacancy-service/src/main/java/ru/somecompany/controller/VacancyController.java @@ -0,0 +1,64 @@ +package ru.somecompany.controller; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.constraints.NotNull; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import ru.somecompany.domain.CreateVacancyRequest; +import ru.somecompany.domain.VacancyEntity; +import ru.somecompany.domain.VacancyResponse; + +import java.util.List; +import java.util.UUID; + +@Validated +@Tag(name = "vacancy", description = "API для управления вакансиями") +public interface VacancyController { + + @Operation(summary = "Получение всех вакансий") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Данные успешно переданы"), + }) + @GetMapping(value = "/api/v1/vacancy") + List getVacancies(); + + @Operation(summary = "Создание вакансии") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Вакансия успешно создана"), + }) + @PostMapping(value = "/api/v1/vacancy") + VacancyEntity createVacancy(@RequestBody @NotNull CreateVacancyRequest request); + + @Operation(summary = "Получение информации о вакансии по id") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Данные успешно переданы"), + @ApiResponse(responseCode = "404", description = "Вакансия не найдена") + }) + @GetMapping(value = "/api/v1/vacancy/{vacancyId}") + VacancyResponse getVacancy(@PathVariable UUID vacancyId); + + @Operation(summary = "Редактирование вакансии") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Данные вакансии успешно изменены"), + @ApiResponse(responseCode = "404", description = "Вакансия не найдена") + }) + @PutMapping(value = "/api/v1/vacancy/{vacancyId}") + VacancyEntity updateVacancy(@PathVariable UUID vacancyId, + @RequestBody @NotNull CreateVacancyRequest request); + + @Operation(summary = "Удалении вакансии по id") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Вакансия успешно удалена"), + @ApiResponse(responseCode = "404", description = "Вакансия не найдена") + }) + @DeleteMapping(value = "/api/v1/vacancy/{vacancyId}") + void deleteVacancy(@PathVariable UUID vacancyId); +} diff --git a/borschevskaya_anna_lab_3/vacancy-service/src/main/java/ru/somecompany/controller/VacancyControllerImpl.java b/borschevskaya_anna_lab_3/vacancy-service/src/main/java/ru/somecompany/controller/VacancyControllerImpl.java new file mode 100644 index 0000000..7713140 --- /dev/null +++ b/borschevskaya_anna_lab_3/vacancy-service/src/main/java/ru/somecompany/controller/VacancyControllerImpl.java @@ -0,0 +1,45 @@ +package ru.somecompany.controller; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.RestController; +import ru.somecompany.domain.CreateVacancyRequest; +import ru.somecompany.domain.VacancyEntity; +import ru.somecompany.domain.VacancyResponse; +import ru.somecompany.service.VacancyService; + +import java.util.List; +import java.util.UUID; + +@Slf4j +@RestController +@RequiredArgsConstructor +public class VacancyControllerImpl implements VacancyController { + + private final VacancyService vacancyService; + + @Override + public List getVacancies() { + return vacancyService.getVacancies(); + } + + @Override + public VacancyEntity createVacancy(CreateVacancyRequest request) { + return vacancyService.createVacancy(request); + } + + @Override + public VacancyResponse getVacancy(UUID vacancyId) { + return vacancyService.getVacancy(vacancyId); + } + + @Override + public VacancyEntity updateVacancy(UUID vacancyId, CreateVacancyRequest request) { + return vacancyService.updateVacancy(vacancyId, request); + } + + @Override + public void deleteVacancy(UUID vacancyId) { + vacancyService.deleteVacancy(vacancyId); + } +} diff --git a/borschevskaya_anna_lab_3/vacancy-service/src/main/java/ru/somecompany/domain/CompanyResponse.java b/borschevskaya_anna_lab_3/vacancy-service/src/main/java/ru/somecompany/domain/CompanyResponse.java new file mode 100644 index 0000000..77fef37 --- /dev/null +++ b/borschevskaya_anna_lab_3/vacancy-service/src/main/java/ru/somecompany/domain/CompanyResponse.java @@ -0,0 +1,17 @@ +package ru.somecompany.domain; + +import lombok.AllArgsConstructor; +import lombok.Data; + +import java.util.UUID; + +@Data +@AllArgsConstructor +public class CompanyResponse { + + private UUID id; + + private String name; + + private String address; +} diff --git a/borschevskaya_anna_lab_3/vacancy-service/src/main/java/ru/somecompany/domain/CreateVacancyRequest.java b/borschevskaya_anna_lab_3/vacancy-service/src/main/java/ru/somecompany/domain/CreateVacancyRequest.java new file mode 100644 index 0000000..1a35235 --- /dev/null +++ b/borschevskaya_anna_lab_3/vacancy-service/src/main/java/ru/somecompany/domain/CreateVacancyRequest.java @@ -0,0 +1,21 @@ +package ru.somecompany.domain; + +import lombok.AllArgsConstructor; +import lombok.Data; + +import java.util.UUID; + +@Data +@AllArgsConstructor +public class CreateVacancyRequest { + + private String name; + + private String description; + + private Integer salaryLow; + + private Integer salaryHigh; + + private UUID companyId; +} diff --git a/borschevskaya_anna_lab_3/vacancy-service/src/main/java/ru/somecompany/domain/VacancyEntity.java b/borschevskaya_anna_lab_3/vacancy-service/src/main/java/ru/somecompany/domain/VacancyEntity.java new file mode 100644 index 0000000..c4fb1c2 --- /dev/null +++ b/borschevskaya_anna_lab_3/vacancy-service/src/main/java/ru/somecompany/domain/VacancyEntity.java @@ -0,0 +1,41 @@ +package ru.somecompany.domain; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.UUID; + +@Data +@Entity +@Builder +@Table(name = "vacancy") +@AllArgsConstructor +@NoArgsConstructor +public class VacancyEntity { + + @Id + @GeneratedValue + private UUID id; + + @Column(name = "name", nullable = false) + private String name; + + @Column(name = "description", nullable = true, length = 1000) + private String description; + + @Column(name = "salary_low", nullable = false) + private Integer salaryLow; + + @Column(name = "salary_high", nullable = false) + private Integer salaryHigh; + + @Column(name = "company_id", nullable = false) + private UUID companyId; +} diff --git a/borschevskaya_anna_lab_3/vacancy-service/src/main/java/ru/somecompany/domain/VacancyResponse.java b/borschevskaya_anna_lab_3/vacancy-service/src/main/java/ru/somecompany/domain/VacancyResponse.java new file mode 100644 index 0000000..56bae2b --- /dev/null +++ b/borschevskaya_anna_lab_3/vacancy-service/src/main/java/ru/somecompany/domain/VacancyResponse.java @@ -0,0 +1,25 @@ +package ru.somecompany.domain; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Builder; +import lombok.Data; + +import java.util.UUID; + +@Data +@Builder +@Schema(description = "Данные по вакансии") +public class VacancyResponse { + + private UUID id; + + private String name; + + private String description; + + private Integer salaryLow; + + private Integer salaryHigh; + + private CompanyResponse company; +} diff --git a/borschevskaya_anna_lab_3/vacancy-service/src/main/java/ru/somecompany/exception/AbstractWebExceptionHandler.java b/borschevskaya_anna_lab_3/vacancy-service/src/main/java/ru/somecompany/exception/AbstractWebExceptionHandler.java new file mode 100644 index 0000000..fc34e6f --- /dev/null +++ b/borschevskaya_anna_lab_3/vacancy-service/src/main/java/ru/somecompany/exception/AbstractWebExceptionHandler.java @@ -0,0 +1,41 @@ +package ru.somecompany.exception; + +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; + +import java.time.LocalDateTime; + +import static org.springframework.http.HttpStatus.NOT_FOUND; + +@Slf4j +public abstract class AbstractWebExceptionHandler { + + public static ResponseEntity buildResponse(ErrorCode errorCode, HttpStatus status, String msg, + boolean details, Throwable ex) { + return buildResponse(errorCode.name(), status, msg, details, ex, LocalDateTime.now()); + } + + public static ResponseEntity buildResponse(String errorCode, HttpStatus status, String msg, + boolean details, Throwable ex, LocalDateTime timestamp) { + if (details) { + log.error(msg, ex); // with stack-trace + } else { + var message = StringUtils.equals(msg, ex.getMessage()) ? msg : msg + ": " + ex.getMessage(); + if (status == NOT_FOUND) { + log.warn(message); + } else { + log.error(message); + } + } + + return ResponseEntity.status(status.value()) + .body(ResponseError.builder() + .code(errorCode) + .message(msg) + .timestamp(timestamp) + .status(status.value()) + .build()); + } +} diff --git a/borschevskaya_anna_lab_3/vacancy-service/src/main/java/ru/somecompany/exception/BasicError.java b/borschevskaya_anna_lab_3/vacancy-service/src/main/java/ru/somecompany/exception/BasicError.java new file mode 100644 index 0000000..baca00a --- /dev/null +++ b/borschevskaya_anna_lab_3/vacancy-service/src/main/java/ru/somecompany/exception/BasicError.java @@ -0,0 +1,5 @@ +package ru.somecompany.exception; + +public enum BasicError implements ErrorCode { + NOT_FOUND +} diff --git a/borschevskaya_anna_lab_3/vacancy-service/src/main/java/ru/somecompany/exception/CompanyAppRuntimeException.java b/borschevskaya_anna_lab_3/vacancy-service/src/main/java/ru/somecompany/exception/CompanyAppRuntimeException.java new file mode 100644 index 0000000..5148716 --- /dev/null +++ b/borschevskaya_anna_lab_3/vacancy-service/src/main/java/ru/somecompany/exception/CompanyAppRuntimeException.java @@ -0,0 +1,26 @@ +package ru.somecompany.exception; + +import lombok.Getter; +import org.springframework.http.HttpStatus; + +@Getter +public class CompanyAppRuntimeException extends RuntimeException { + private final ErrorCode errorCode; + private final HttpStatus status; + private final boolean details; + + public CompanyAppRuntimeException(ErrorCode errorCode, HttpStatus status, String message) { + this(errorCode, status, message, null); + } + + public CompanyAppRuntimeException(ErrorCode errorCode, HttpStatus status, String message, Throwable error) { + this(errorCode, status, message, true, error); + } + + public CompanyAppRuntimeException(ErrorCode errorCode, HttpStatus status, String message, boolean details, Throwable error) { + super(message, error); + this.errorCode = errorCode; + this.status = status; + this.details = details; + } +} diff --git a/borschevskaya_anna_lab_3/vacancy-service/src/main/java/ru/somecompany/exception/ErrorCode.java b/borschevskaya_anna_lab_3/vacancy-service/src/main/java/ru/somecompany/exception/ErrorCode.java new file mode 100644 index 0000000..ed73439 --- /dev/null +++ b/borschevskaya_anna_lab_3/vacancy-service/src/main/java/ru/somecompany/exception/ErrorCode.java @@ -0,0 +1,9 @@ +package ru.somecompany.exception; + +import java.io.Serializable; + +public interface ErrorCode extends Serializable { + + String name(); + +} diff --git a/borschevskaya_anna_lab_3/vacancy-service/src/main/java/ru/somecompany/exception/ResourceNotFoundException.java b/borschevskaya_anna_lab_3/vacancy-service/src/main/java/ru/somecompany/exception/ResourceNotFoundException.java new file mode 100644 index 0000000..7ecd7c0 --- /dev/null +++ b/borschevskaya_anna_lab_3/vacancy-service/src/main/java/ru/somecompany/exception/ResourceNotFoundException.java @@ -0,0 +1,28 @@ +package ru.somecompany.exception; + +import org.springframework.http.HttpStatus; + +import static ru.somecompany.exception.BasicError.NOT_FOUND; + +public class ResourceNotFoundException extends CompanyAppRuntimeException { + + public ResourceNotFoundException(ErrorCode errorCode, String message) { + this(errorCode, message, null); + } + + public ResourceNotFoundException(ErrorCode errorCode, String message, Throwable error) { + super(errorCode, HttpStatus.NOT_FOUND, message, error); + } + + public static ResourceNotFoundException notFound(String message, Object... args) { + return new ResourceNotFoundException(NOT_FOUND, String.format(message, args), null); + } + + public static ResourceNotFoundException notFound() { + return new ResourceNotFoundException(NOT_FOUND, "Запрашиваемые данные не найдены", null); + } + + public static ResourceNotFoundException notFound(Throwable error) { + return new ResourceNotFoundException(NOT_FOUND, error.getMessage(), error); + } +} \ No newline at end of file diff --git a/borschevskaya_anna_lab_3/vacancy-service/src/main/java/ru/somecompany/exception/ResponseError.java b/borschevskaya_anna_lab_3/vacancy-service/src/main/java/ru/somecompany/exception/ResponseError.java new file mode 100644 index 0000000..4ae118d --- /dev/null +++ b/borschevskaya_anna_lab_3/vacancy-service/src/main/java/ru/somecompany/exception/ResponseError.java @@ -0,0 +1,24 @@ +package ru.somecompany.exception; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; + +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class ResponseError { + @JsonProperty(required = true) + private LocalDateTime timestamp; + @JsonProperty(required = true) + private Integer status; + @JsonProperty(required = true) + private String message; + @JsonProperty(required = true) + private String code; +} diff --git a/borschevskaya_anna_lab_3/vacancy-service/src/main/java/ru/somecompany/exception/WebExceptionHandler.java b/borschevskaya_anna_lab_3/vacancy-service/src/main/java/ru/somecompany/exception/WebExceptionHandler.java new file mode 100644 index 0000000..78fa51f --- /dev/null +++ b/borschevskaya_anna_lab_3/vacancy-service/src/main/java/ru/somecompany/exception/WebExceptionHandler.java @@ -0,0 +1,22 @@ +package ru.somecompany.exception; + +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +@Slf4j +@Getter +@RestControllerAdvice +@RequiredArgsConstructor +public class WebExceptionHandler extends AbstractWebExceptionHandler { + private final ObjectMapper objectMapper; + + @ExceptionHandler(CompanyAppRuntimeException.class) + public ResponseEntity handleException(CompanyAppRuntimeException ex) { + return buildResponse(ex.getErrorCode(), ex.getStatus(), ex.getMessage(), ex.isDetails(), ex); + } +} diff --git a/borschevskaya_anna_lab_3/vacancy-service/src/main/java/ru/somecompany/external/CompanyClient.java b/borschevskaya_anna_lab_3/vacancy-service/src/main/java/ru/somecompany/external/CompanyClient.java new file mode 100644 index 0000000..17eed7b --- /dev/null +++ b/borschevskaya_anna_lab_3/vacancy-service/src/main/java/ru/somecompany/external/CompanyClient.java @@ -0,0 +1,15 @@ +package ru.somecompany.external; + +import org.springframework.cloud.openfeign.FeignClient; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import ru.somecompany.domain.CompanyResponse; + +import java.util.UUID; + +@FeignClient(name = "company", url = "${app.feign.company-url}") +public interface CompanyClient { + + @GetMapping(value = "/api/v1/company/{companyId}") + CompanyResponse getCompany(@PathVariable UUID companyId); +} diff --git a/borschevskaya_anna_lab_3/vacancy-service/src/main/java/ru/somecompany/mapper/VacancyMapper.java b/borschevskaya_anna_lab_3/vacancy-service/src/main/java/ru/somecompany/mapper/VacancyMapper.java new file mode 100644 index 0000000..2be7961 --- /dev/null +++ b/borschevskaya_anna_lab_3/vacancy-service/src/main/java/ru/somecompany/mapper/VacancyMapper.java @@ -0,0 +1,24 @@ +package ru.somecompany.mapper; + +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.MappingTarget; +import ru.somecompany.domain.CompanyResponse; +import ru.somecompany.domain.CreateVacancyRequest; +import ru.somecompany.domain.VacancyEntity; +import ru.somecompany.domain.VacancyResponse; + +import java.util.UUID; + +@Mapper(imports = UUID.class) +public interface VacancyMapper { + + @Mapping(target = "company", source = "company") + @Mapping(target = "name", source = "entity.name") + @Mapping(target = "id", source = "entity.id") + VacancyResponse map(VacancyEntity entity, CompanyResponse company); + + VacancyEntity map(CreateVacancyRequest request); + + VacancyEntity map(@MappingTarget VacancyEntity entity, CreateVacancyRequest request); +} diff --git a/borschevskaya_anna_lab_3/vacancy-service/src/main/java/ru/somecompany/repository/VacancyRepository.java b/borschevskaya_anna_lab_3/vacancy-service/src/main/java/ru/somecompany/repository/VacancyRepository.java new file mode 100644 index 0000000..5d68d48 --- /dev/null +++ b/borschevskaya_anna_lab_3/vacancy-service/src/main/java/ru/somecompany/repository/VacancyRepository.java @@ -0,0 +1,11 @@ +package ru.somecompany.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; +import ru.somecompany.domain.VacancyEntity; + +import java.util.UUID; + +@Repository +public interface VacancyRepository extends JpaRepository { +} diff --git a/borschevskaya_anna_lab_3/vacancy-service/src/main/java/ru/somecompany/service/VacancyService.java b/borschevskaya_anna_lab_3/vacancy-service/src/main/java/ru/somecompany/service/VacancyService.java new file mode 100644 index 0000000..0ea9def --- /dev/null +++ b/borschevskaya_anna_lab_3/vacancy-service/src/main/java/ru/somecompany/service/VacancyService.java @@ -0,0 +1,21 @@ +package ru.somecompany.service; + +import ru.somecompany.domain.CreateVacancyRequest; +import ru.somecompany.domain.VacancyEntity; +import ru.somecompany.domain.VacancyResponse; + +import java.util.List; +import java.util.UUID; + +public interface VacancyService { + + List getVacancies(); + + VacancyEntity createVacancy(CreateVacancyRequest request); + + VacancyEntity updateVacancy(UUID vacancyId, CreateVacancyRequest request); + + void deleteVacancy(UUID vacancyId); + + VacancyResponse getVacancy(UUID vacancyId); +} diff --git a/borschevskaya_anna_lab_3/vacancy-service/src/main/java/ru/somecompany/service/VacancyServiceImpl.java b/borschevskaya_anna_lab_3/vacancy-service/src/main/java/ru/somecompany/service/VacancyServiceImpl.java new file mode 100644 index 0000000..db58bdd --- /dev/null +++ b/borschevskaya_anna_lab_3/vacancy-service/src/main/java/ru/somecompany/service/VacancyServiceImpl.java @@ -0,0 +1,68 @@ +package ru.somecompany.service; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import ru.somecompany.domain.CreateVacancyRequest; +import ru.somecompany.domain.VacancyEntity; +import ru.somecompany.domain.VacancyResponse; +import ru.somecompany.mapper.VacancyMapper; +import ru.somecompany.repository.VacancyRepository; +import ru.somecompany.service.adapter.CompanyAdapter; + +import java.util.List; +import java.util.UUID; + +import static ru.somecompany.exception.ResourceNotFoundException.notFound; + +@Slf4j +@Service +@RequiredArgsConstructor +public class VacancyServiceImpl implements VacancyService { + + private final VacancyRepository vacancyRepository; + private final CompanyAdapter companyAdapter; + private final VacancyMapper mapper; + + @Override + public List getVacancies() { + return vacancyRepository.findAll(); + } + + @Override + public VacancyEntity createVacancy(CreateVacancyRequest request) { + var entity = mapper.map(request); + return vacancyRepository.save(entity); + } + + @Override + public VacancyEntity updateVacancy(UUID vacancyId, CreateVacancyRequest request) { + var entity = getById(vacancyId); + entity = mapper.map(entity, request); + + return vacancyRepository.save(entity); + } + + @Override + public void deleteVacancy(UUID vacancyId) { + var entity = getById(vacancyId); + vacancyRepository.delete(entity); + } + + @Override + public VacancyResponse getVacancy(UUID vacancyId) { + var entity = getById(vacancyId); + var company = companyAdapter.getCompanyById(entity.getCompanyId()); + + return mapper.map(entity, company); + } + + private VacancyEntity getById(UUID vacancyId) { + var entity = vacancyRepository.findById(vacancyId); + if (entity.isEmpty()) { + log.warn("The vacancy with id '{}' was not found", vacancyId); + throw notFound("Вакансия не найдена"); + } + return entity.get(); + } +} diff --git a/borschevskaya_anna_lab_3/vacancy-service/src/main/java/ru/somecompany/service/adapter/CompanyAdapter.java b/borschevskaya_anna_lab_3/vacancy-service/src/main/java/ru/somecompany/service/adapter/CompanyAdapter.java new file mode 100644 index 0000000..1918a3c --- /dev/null +++ b/borschevskaya_anna_lab_3/vacancy-service/src/main/java/ru/somecompany/service/adapter/CompanyAdapter.java @@ -0,0 +1,10 @@ +package ru.somecompany.service.adapter; + +import ru.somecompany.domain.CompanyResponse; + +import java.util.UUID; + +public interface CompanyAdapter { + + CompanyResponse getCompanyById(UUID companyId); +} diff --git a/borschevskaya_anna_lab_3/vacancy-service/src/main/java/ru/somecompany/service/adapter/CompanyAdapterImpl.java b/borschevskaya_anna_lab_3/vacancy-service/src/main/java/ru/somecompany/service/adapter/CompanyAdapterImpl.java new file mode 100644 index 0000000..ca18674 --- /dev/null +++ b/borschevskaya_anna_lab_3/vacancy-service/src/main/java/ru/somecompany/service/adapter/CompanyAdapterImpl.java @@ -0,0 +1,20 @@ +package ru.somecompany.service.adapter; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import ru.somecompany.domain.CompanyResponse; +import ru.somecompany.external.CompanyClient; + +import java.util.UUID; + +@Service +@RequiredArgsConstructor +public class CompanyAdapterImpl implements CompanyAdapter { + + private final CompanyClient companyClient; + + @Override + public CompanyResponse getCompanyById(UUID companyId) { + return companyClient.getCompany(companyId); + } +} diff --git a/borschevskaya_anna_lab_3/vacancy-service/src/main/resources/application.yml b/borschevskaya_anna_lab_3/vacancy-service/src/main/resources/application.yml new file mode 100644 index 0000000..afdc22e --- /dev/null +++ b/borschevskaya_anna_lab_3/vacancy-service/src/main/resources/application.yml @@ -0,0 +1,32 @@ +server: + port: ${SERVER_PORT:8080} +spring: + application: + name: vacancy-app + jpa: + database: POSTGRESQL + open-in-view: false + show-sql: false + hibernate: + ddl-auto: none + properties: + hibernate: + dialect: org.hibernate.dialect.PostgreSQLDialect + datasource: + url: ${DB_URL:jdbc:postgresql://localhost:5433/vacancy} + username: ${DB_USERNAME:postgres} + password: ${DB_PASSWORD:postgres} + driverClassName: org.postgresql.Driver + type: com.zaxxer.hikari.HikariDataSource + hikari: + maximum-pool-size: 15 + minimum-idle: 4 + idle-timeout: 180000 + max-lifetime: 599000 + flyway: + enabled: true + locations: db/migration/ + +app: + feign: + company-url: ${COMPANY_URL:http://localhost:8081} \ No newline at end of file diff --git a/borschevskaya_anna_lab_3/vacancy-service/src/main/resources/db/migration/V000001__init.sql b/borschevskaya_anna_lab_3/vacancy-service/src/main/resources/db/migration/V000001__init.sql new file mode 100644 index 0000000..983c6a9 --- /dev/null +++ b/borschevskaya_anna_lab_3/vacancy-service/src/main/resources/db/migration/V000001__init.sql @@ -0,0 +1,9 @@ +CREATE TABLE vacancy ( + id UUID + CONSTRAINT company_pk PRIMARY KEY, + name VARCHAR(255) NOT NULL, + description VARCHAR(1000), + salary_low int8 NOT NULL, + salary_high int8 NOT NULL, + company_id UUID NOT NULL +);