diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100644 index 0000000000000000000000000000000000000000..378dfb67de3421235066750fd30bcd7980e3845f --- /dev/null +++ b/.gitlab-ci.yml @@ -0,0 +1,53 @@ +image: 'maven:3.8.5-openjdk-17-slim' + +cache: + paths: + - .m2/repository + +variables: + MAVEN_OPTS: "-Dmaven.repo.local=$CI_PROJECT_DIR/.m2/repository" + MAVEN_CLI_OPTS: "--batch-mode --errors --fail-at-end --show-version" + +stages: + - build + - unit_test + - integration_test + +build: + tags: + - shared-fi + stage: build + script: + - echo "We are building your project, $GITLAB_USER_LOGIN" + - ls + - ./mvnw clean install -Dmaven.test.skip=true $MAVEN_CLI_OPTS + +unit_test: + tags: + - shared-fi + stage: unit_test + script: + - echo "We are testing your project build with unit tests, $GITLAB_USER_LOGIN" + - ./mvnw test $MAVEN_CLI_OPTS + artifacts: + expire_in: 10 min + paths: + - "*/target/surefire-reports/*" + reports: + junit: + - "*/target/surefire-reports/*.xml" + +integration_test: + tags: + - shared-fi + stage: integration_test + script: + - echo "We are testing your project build with integration tests, $GITLAB_USER_LOGIN" + - ./mvnw verify $MAVEN_CLI_OPTS + artifacts: + expire_in: 10 min + paths: + - "*/target/failsafe-reports/*" + reports: + junit: + - "*/target/failsafe-reports/*.xml" \ No newline at end of file diff --git a/.mvn/wrapper/maven-wrapper.jar b/.mvn/wrapper/maven-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..cb28b0e37c7d206feb564310fdeec0927af4123a Binary files /dev/null and b/.mvn/wrapper/maven-wrapper.jar differ diff --git a/.mvn/wrapper/maven-wrapper.properties b/.mvn/wrapper/maven-wrapper.properties new file mode 100644 index 0000000000000000000000000000000000000000..08ea486aa5a85d4894899b7e3aae7c51e68e161a --- /dev/null +++ b/.mvn/wrapper/maven-wrapper.properties @@ -0,0 +1,18 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.0/apache-maven-3.9.0-bin.zip +wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar diff --git a/README.md b/README.md index 66d3859762ee745ab74ebf1f6721b3cb2ba8a1a3..92b3b9b4ee61aa00abef0d43a4deb5a2e7c816e5 100644 --- a/README.md +++ b/README.md @@ -11,12 +11,50 @@ Create an information system managing flight records at an airport. The system s   -## Installation +## Installation/Running with maven ### To build with Maven `$mvn clean install` ### To run app on localhost `$mvn spring-boot:run - in each microservice separatly` +## To run using Docker/Podman compose +### To build with Maven + $ mvn clean install +### If you want to force the images to be rebuild you can use + $ podman image list | grep "^localhost/pa165-airport-project_" | awk '{print $3}' | xargs podman image rm +This will delete existing images. + +### To run the application (all microservices) + $ podman-compose up + +### To stop the application (all microservices) + $ podman-compose down + +## Observations + +### Prometheus + +The prometheus collects the metrics from all 4 microservices. The UI runs at the http://localhost:9090. + +### Grafana + +Grafana runs at http://localhost:3000. To Log in, use the username `admin` and the password `admin`. +There is already one minimalistic dashboard imported. However, feel free to experiment and visualize another attributes. + +## Scenarios + +### Run scenarios +Firstly run the app + $ cd locust-scenarios + $ locust Admin BasicUser +Open internet browser +Type http://localhost:8089 +Set: + - Number of users 2 + - Spawn rate 1 + - Host http://localhost + + ## Usage ### Used ports + Authorization - port 8083 diff --git a/authorization/openapi.yaml b/authorization/openapi.yaml deleted file mode 100644 index e9a842cb345cee79bf4a88c9d601de1a29ef40c4..0000000000000000000000000000000000000000 --- a/authorization/openapi.yaml +++ /dev/null @@ -1,301 +0,0 @@ -openapi: 3.0.3 -info: - title: Airport Manager authorization microservice - description: Airport Manager authorization microservice - version: 1.0.0 -servers: - - url: "{scheme}://{server}:{port}" - description: my server - variables: - scheme: - default: http - enum: - - http - - https - server: - default: localhost - port: - default: "${server.port}" -paths: - /api/users/: - get: - tags: - - User - summary: List all system users - description: Get an array of all system users - operationId: getAllUsers - responses: - "200": - description: OK - content: - application/json: - schema: - type: array - items: - $ref: '#/components/schemas/UserDto' - put: - tags: - - User - summary: Create user - description: Create a new user and return it. - operationId: createUser - requestBody: - description: Data to create a new user - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/CreationUserDto' - responses: - "200": - description: OK - content: - application/json: - schema: - $ref: '#/components/schemas/UserDto' - "400": - description: Invalid input - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorMessage' - - /api/users/{id}: - get: - tags: - - User - summary: Get user by id. - description: Returns an object representing an user. - operationId: getUserById - parameters: - - name: id - in: path - required: true - schema: - type: integer - format: int64 - responses: - "200": - description: OK - content: - application/json: - schema: - $ref: '#/components/schemas/UserDto' - "404": - description: Not Found - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorMessage' - put: - tags: - - User - summary: Update user by id. - description: Update a user - operationId: updateUserById - requestBody: - description: Data to create a new user - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/CreationUserDto' - parameters: - - name: id - in: path - required: true - schema: - type: integer - format: int64 - responses: - "200": - description: OK - content: - application/json: - schema: - $ref: '#/components/schemas/UserDto' - "404": - description: Not Found - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorMessage' - "400": - description: Invalid input - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorMessage' - delete: - tags: - - User - summary: Delete user by id. - description: delete a user - operationId: deleteUserById - parameters: - - name: id - in: path - required: true - schema: - type: integer - format: int64 - responses: - "200": - description: OK - "404": - description: Not Found - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorMessage' - /api/login: - put: - tags: - - User - summary: Login to application - description: Login to application and return an authentication token - operationId: login - requestBody: - description: Login name and password - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/LoginDto' - responses: - "200": - description: OK - content: - application/json: - schema: - $ref: '#/components/schemas/LoginResponse' - "400": - description: Invalid input - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorMessage' -components: - schemas: - DomainEntity: - title: Domain Entity - description: Represents a Domain Entity - type: object - required: - - id - properties: - id: - type: integer - description: unique id - format: int64 - example: 1 - discriminator: - propertyName: objectType - - ErrorMessage: - title: Error Message - description: Response body for HTML statuses. - type: object - properties: - message: - type: string - description: reason for error - example: entity not found - - UserDto: - allOf: - - $ref: '#/components/schemas/DomainEntity' - type: object - title: User - description: Represents a system user. - required: - - login - - firstName - - lastName - - email - - role - properties: - login: - type: string - description: name used for logging in - example: john.doe - firstName: - type: string - description: first name of a the user - example: John - lastName: - type: string - description: last name of a user - example: Doe - email: - type: string - description: email address of the user - example: john@example.com - role: - description: the role of the user - type: string - # FIXME: is this an openapi bug or just wrong syntax ? https://github.com/OpenAPITools/openapi-generator/issues/11323 - #enum: - # - manager - # - admin - CreationUserDto: - type: object - title: User - description: Represents a user. - required: - - login - - firstName - - lastName - - password - - email - - role - properties: - login: - type: string - description: name used for logging in - example: john.doe - firstName: - type: string - description: first name of a the user - example: John - lastName: - type: string - description: last name of a user - example: Doe - password: - type: string - description: password of the user - example: secretPassword - email: - type: string - description: email address of the user - example: john@example.com - role: - type: string - # TODO: enum - LoginDto: - type: object - title: User - description: Represents login information for a user. - required: - - login - - password - properties: - login: - type: string - description: name used for logging in - example: john.doe - password: - type: string - description: password of the user - example: secretPassword - LoginResponse: - type: object - title: User - description: Represents a login response - required: - - token - properties: - token: - type: string - description: token used for authentication \ No newline at end of file diff --git a/authorization/src/main/java/cz/muni/fi/pa165/authorization/server/rest/UserController.java b/authorization/src/main/java/cz/muni/fi/pa165/authorization/server/rest/UserController.java deleted file mode 100644 index e3921a3de83a660fea68daed83bb7aa15045a49a..0000000000000000000000000000000000000000 --- a/authorization/src/main/java/cz/muni/fi/pa165/authorization/server/rest/UserController.java +++ /dev/null @@ -1,55 +0,0 @@ -package cz.muni.fi.pa165.authorization.server.rest; - -import cz.muni.fi.pa165.authorization.server.api.UserApiDelegate; -import cz.muni.fi.pa165.authorization.server.model.CreationUserDto; -import cz.muni.fi.pa165.authorization.server.model.LoginDto; -import cz.muni.fi.pa165.authorization.server.model.LoginResponse; -import cz.muni.fi.pa165.authorization.server.model.UserDto; -import cz.muni.fi.pa165.authorization.server.service.UserService; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.RestController; - -import java.util.List; - -@RestController -public class UserController implements UserApiDelegate { - - private final UserService userService; - - @Autowired - public UserController(UserService userService) { - this.userService = userService; - } - - @Override - public ResponseEntity<UserDto> getUserById(Long id) { - return ResponseEntity.ok(userService.getUserById(id)); - } - - @Override - public ResponseEntity<UserDto> createUser(CreationUserDto creationUserDto) { - return ResponseEntity.ok(userService.createUser(creationUserDto)); - } - - @Override - public ResponseEntity<UserDto> updateUserById(Long id, CreationUserDto creationUserDto) { - return ResponseEntity.ok(userService.updateUserById(id, creationUserDto)); - } - - @Override - public ResponseEntity<Void> deleteUserById(Long id) { - userService.deleteUserById(id); - return ResponseEntity.ok(null); - } - - @Override - public ResponseEntity<LoginResponse> login(LoginDto loginDto) { - return ResponseEntity.ok(userService.login(loginDto)); - } - - @Override - public ResponseEntity<List<UserDto>> getAllUsers() { - return ResponseEntity.ok(userService.getAllUsers()); - } -} diff --git a/authorization/src/main/java/cz/muni/fi/pa165/authorization/server/service/UserService.java b/authorization/src/main/java/cz/muni/fi/pa165/authorization/server/service/UserService.java deleted file mode 100644 index dbe944782304fdf4d98a512dd32ca79a6d0f2174..0000000000000000000000000000000000000000 --- a/authorization/src/main/java/cz/muni/fi/pa165/authorization/server/service/UserService.java +++ /dev/null @@ -1,56 +0,0 @@ -package cz.muni.fi.pa165.authorization.server.service; - -import cz.muni.fi.pa165.authorization.server.model.CreationUserDto; -import cz.muni.fi.pa165.authorization.server.model.LoginDto; -import cz.muni.fi.pa165.authorization.server.model.LoginResponse; -import cz.muni.fi.pa165.authorization.server.model.UserDto; -import org.springframework.http.ResponseEntity; -import org.springframework.stereotype.Service; - -import java.util.List; - -@Service -public class UserService { - - public UserDto getUserById(Long id) { - return new UserDto() - .id(id) - .login("john.doe") - .firstName("John") - .lastName("Doe") - .email("john@example.com") - .role("manager"); - } - - public UserDto createUser(CreationUserDto creationUserDto) { - return new UserDto() - .id(1L) - .login(creationUserDto.getLogin()) - .firstName(creationUserDto.getFirstName()) - .lastName(creationUserDto.getLastName()) - .email(creationUserDto.getEmail()) - .role(creationUserDto.getRole()); - } - - public UserDto updateUserById(Long id, CreationUserDto creationUserDto) { - return new UserDto() - .id(id) - .login(creationUserDto.getLogin()) - .firstName(creationUserDto.getFirstName()) - .lastName(creationUserDto.getLastName()) - .email(creationUserDto.getEmail()) - .role(creationUserDto.getRole()); - } - - public void deleteUserById(Long id) { - - } - - public LoginResponse login(LoginDto loginDto) { - return new LoginResponse().token("token-for-" + loginDto.getLogin()); - } - - public List<UserDto> getAllUsers() { - return List.of(getUserById(1L)); - } -} diff --git a/authorization/src/main/resources/application.yml b/authorization/src/main/resources/application.yml deleted file mode 100644 index 86820bd5b40c31e85ed3ec1d44cc4cba01a5cfd4..0000000000000000000000000000000000000000 --- a/authorization/src/main/resources/application.yml +++ /dev/null @@ -1,10 +0,0 @@ -spring: - datasource: - url: jdbc:h2:mem:exampleDb - username: sa - password: password - driverClassName: org.h2.Driver - jpa: - database-platform: org.hibernate.dialect.H2Dialect -server: - port: 8083 \ No newline at end of file diff --git a/authorization/src/test/java/cz/muni/fi/pa165/authorization/server/UsersIT.java b/authorization/src/test/java/cz/muni/fi/pa165/authorization/server/UsersIT.java deleted file mode 100644 index 7a1ce4adb605f74052b224e93f7267ab2dae3c4f..0000000000000000000000000000000000000000 --- a/authorization/src/test/java/cz/muni/fi/pa165/authorization/server/UsersIT.java +++ /dev/null @@ -1,168 +0,0 @@ -package cz.muni.fi.pa165.authorization.server; - -import com.fasterxml.jackson.databind.ObjectMapper; -import cz.muni.fi.pa165.authorization.server.model.LoginResponse; -import cz.muni.fi.pa165.authorization.server.model.UserDto; -import org.junit.jupiter.api.Test; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.http.MediaType; -import org.springframework.test.web.servlet.MockMvc; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; - -@SpringBootTest -@AutoConfigureMockMvc -class UsersIT { - - private static final Logger log = LoggerFactory.getLogger(UsersIT.class); - - @Autowired - private MockMvc mockMvc; - - @Autowired - private ObjectMapper objectMapper; - - @Test - void getUserByIdTest() throws Exception { - log.debug("getUserByIdTest() running"); - String response = mockMvc.perform(get("/api/users/1")) - .andExpect(status().isOk()) - .andExpect(jsonPath("$.firstName").value("John")) - .andExpect(jsonPath("$.lastName").value("Doe")) - .andExpect(jsonPath("$.login").value("john.doe")) - .andExpect(jsonPath("$.email").value("john@example.com")) - .andExpect(jsonPath("$.role").value("manager")) - .andReturn().getResponse().getContentAsString(); - log.debug("response: {}", response); - UserDto responseDto = objectMapper.readValue(response, UserDto.class); - assertThat(responseDto.getFirstName()).isEqualTo("John"); - assertThat(responseDto.getLastName()).isEqualTo("Doe"); - assertThat(responseDto.getLogin()).isEqualTo("john.doe"); - assertThat(responseDto.getEmail()).isEqualTo("john@example.com"); - } - - @Test - void getAllUsers() throws Exception { - log.debug("getAllUsers() running"); - String response = mockMvc.perform(get("/api/users/")) - .andExpect(status().isOk()) - .andExpect(jsonPath("$[0].firstName").value("John")) - .andExpect(jsonPath("$[0].lastName").value("Doe")) - .andExpect(jsonPath("$[0].login").value("john.doe")) - .andExpect(jsonPath("$[0].email").value("john@example.com")) - .andExpect(jsonPath("$[0].role").value("manager")) - .andReturn().getResponse().getContentAsString(); - log.debug("response: {}", response); - UserDto[] responseDtos = objectMapper.readValue(response, UserDto[].class); - assertThat(responseDtos[0].getFirstName()).isEqualTo("John"); - assertThat(responseDtos[0].getLastName()).isEqualTo("Doe"); - assertThat(responseDtos[0].getLogin()).isEqualTo("john.doe"); - assertThat(responseDtos[0].getEmail()).isEqualTo("john@example.com"); - } - - @Test - void createUser() throws Exception { - log.debug("createUser() running"); - String requestBody = """ - { - "login": "john.doe", - "firstName": "John", - "lastName": "Doe", - "password": "secretPassword", - "email": "john@example.com", - "role": "manager" - }"""; - - String response = mockMvc.perform( - put("/api/users/") - .contentType(MediaType.APPLICATION_JSON) - .content(requestBody)) - .andExpect(status().isOk()) - .andExpect(jsonPath("$.id").value(1L)) - .andExpect(jsonPath("$.firstName").value("John")) - .andExpect(jsonPath("$.lastName").value("Doe")) - .andExpect(jsonPath("$.login").value("john.doe")) - .andExpect(jsonPath("$.email").value("john@example.com")) - .andExpect(jsonPath("$.role").value("manager")) - .andReturn().getResponse().getContentAsString(); - log.debug("response: {}", response); - UserDto responseDto = objectMapper.readValue(response, UserDto.class); - assertThat(responseDto.getId()).isEqualTo(1L); - assertThat(responseDto.getFirstName()).isEqualTo("John"); - assertThat(responseDto.getLastName()).isEqualTo("Doe"); - assertThat(responseDto.getLogin()).isEqualTo("john.doe"); - assertThat(responseDto.getEmail()).isEqualTo("john@example.com"); - } - - @Test - void updateUserById() throws Exception { - log.debug("updateUserByIdTest() running"); - String requestBody = """ - { - "login": "john.doe", - "firstName": "John", - "lastName": "Doe", - "password": "secretPassword", - "email": "john@example.com", - "role": "manager" - }"""; - - String response = mockMvc.perform( - put("/api/users/1") - .contentType(MediaType.APPLICATION_JSON) - .content(requestBody)) - .andExpect(status().isOk()) - .andExpect(jsonPath("$.firstName").value("John")) - .andExpect(jsonPath("$.lastName").value("Doe")) - .andExpect(jsonPath("$.login").value("john.doe")) - .andExpect(jsonPath("$.email").value("john@example.com")) - .andExpect(jsonPath("$.role").value("manager")) - .andReturn().getResponse().getContentAsString(); - log.debug("response: {}", response); - UserDto responseDto = objectMapper.readValue(response, UserDto.class); - assertThat(responseDto.getFirstName()).isEqualTo("John"); - assertThat(responseDto.getLastName()).isEqualTo("Doe"); - assertThat(responseDto.getLogin()).isEqualTo("john.doe"); - assertThat(responseDto.getEmail()).isEqualTo("john@example.com"); - } - - @Test - void deleteUser() throws Exception { - log.debug("deleteUserBy() running"); - - String response = mockMvc.perform( - delete("/api/users/1")) - .andExpect(status().isOk()) - .andReturn().getResponse().getContentAsString(); - log.debug("response: {}", response); - } - - @Test - void login() throws Exception { - log.debug("login() running"); - String requestBody = """ - { - "login": "john.doe", - "password": "password" - }"""; - - String response = mockMvc.perform( - put("/api/login") - .contentType(MediaType.APPLICATION_JSON) - .content(requestBody)) - .andExpect(status().isOk()) - .andExpect(jsonPath("$.token").value("token-for-john.doe")) - .andReturn().getResponse().getContentAsString(); - log.debug("response: {}", response); - LoginResponse responseDto = objectMapper.readValue(response, LoginResponse.class); - assertThat(responseDto.getToken()).isEqualTo("token-for-john.doe"); - } - -} diff --git a/compose.yaml b/compose.yaml new file mode 100644 index 0000000000000000000000000000000000000000..47a0610f271a7d7f2e24ec38bf44e361c1f922af --- /dev/null +++ b/compose.yaml @@ -0,0 +1,46 @@ +version: "3" + +networks: + grafana-prometheus: + driver: bridge + +services: + core: + build: "./core" + ports: + - "8080:8080" + + authorization: + build: "./user" + ports: + - "8083:8083" + + report: + build: "./report" + ports: + - "8085:8085" + + weather: + build: "./weather" + ports: + - "8088:8088" + + prometheus: + image: prom/prometheus:v2.43.0 + volumes: + - ./prometheus.yml:/etc/prometheus/prometheus.yml + command: --config.file=/etc/prometheus/prometheus.yml + networks: + - grafana-prometheus + ports: + - '9090:9090' + + grafana: + image: grafana/grafana:9.1.7 + volumes: + - ./datasource.yml:/etc/grafana/provisioning/datasources/datasource.yml + - ./dashboards:/etc/grafana/provisioning/dashboards + networks: + - grafana-prometheus + ports: + - '3000:3000' diff --git a/core/Dockerfile b/core/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..d131222e38617e1c16b7e0e8e265b68f253b1849 --- /dev/null +++ b/core/Dockerfile @@ -0,0 +1,3 @@ +FROM eclipse-temurin:17-jdk-alpine +COPY target/*.jar /app.jar +ENTRYPOINT ["java", "-jar", "/app.jar"] \ No newline at end of file diff --git a/core/openapi.yaml b/core/openapi.yaml index 9caaa678d0b3b1818303ac4f95039ce47afd7504..f719cd6c8d88f8e414caaebfac9790c90051c2c9 100644 --- a/core/openapi.yaml +++ b/core/openapi.yaml @@ -27,6 +27,8 @@ servers: tags: - name: Core description: Microservice for core. +security: + - BearerAuth: [] paths: /api/stewards: get: @@ -172,33 +174,6 @@ paths: application/json: schema: $ref: '#/components/schemas/ErrorMessage' - put: - tags: - - Steward - summary: Update assignment of steward to a flight. - operationId: updateStewardFlights - parameters: - - name: stewardId - in: path - required: true - schema: - type: integer - format: int64 - - name: flightId - in: path - required: true - schema: - type: integer - format: int64 - responses: - "200": - $ref: '#/components/responses/SingleStewardDtoResponse' - "400": - description: Input data not correct - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorMessage' delete: tags: - Steward @@ -488,7 +463,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/AirplaneTypeDto' + $ref: '#/components/schemas/NewAirplaneTypeDtoRequest' required: true responses: "200": @@ -544,6 +519,12 @@ paths: application/json: schema: $ref: '#/components/schemas/ErrorMessage' + "404": + description: Airplane type not Found + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorMessage' /api/airplanes/{id}: get: tags: @@ -565,6 +546,12 @@ paths: application/json: schema: $ref: '#/components/schemas/AirplaneDto' + "404": + description: Not Found + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorMessage' delete: tags: - Airplane @@ -603,7 +590,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/AirplaneDto' + $ref: '#/components/schemas/NewAirplaneDtoRequest' required: true responses: "200": @@ -618,92 +605,8 @@ paths: application/json: schema: $ref: '#/components/schemas/ErrorMessage' - /api/airplanes/{airplaneId}/airplaneTypes/{airplaneTypeId}: - post: - tags: - - Airplane - summary: Assign airplane type to a airplane. - operationId: assignAirplaneType - parameters: - - name: airplaneId - in: path - required: true - schema: - type: integer - format: int64 - - name: airplaneTypeId - in: path - required: true - schema: - type: integer - format: int64 - responses: - "201": - description: OK - content: - application/json: - schema: - $ref: '#/components/schemas/AirplaneDto' - "400": - description: Input data not correct - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorMessage' - put: - tags: - - Airplane - summary: Update assignment of airplane type to an airplane. - operationId: updateAirplaneTypeAssignment - parameters: - - name: airplaneId - in: path - required: true - schema: - type: integer - format: int64 - - name: airplaneTypeId - in: path - required: true - schema: - type: integer - format: int64 - responses: - "200": - description: OK - content: - application/json: - schema: - $ref: '#/components/schemas/AirplaneDto' - "400": - description: Input data not correct - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorMessage' - delete: - tags: - - Airplane - summary: Delete assignment of airplane type to an airplane. - operationId: deleteAirplaneTypeAssignment - parameters: - - name: airplaneId - in: path - required: true - schema: - type: integer - format: int64 - - name: airplaneTypeId - in: path - required: true - schema: - type: integer - format: int64 - responses: - "204": - description: Deleted "404": - description: Not Found + description: Airplane type not Found content: application/json: schema: @@ -808,7 +711,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/CountryDto' + $ref: '#/components/schemas/NewCountryDtoRequest' required: true responses: "200": @@ -826,7 +729,7 @@ paths: /api/cities: get: tags: - - Country + - City summary: Get all cities. description: Returns an array of objects representing cities. operationId: getAllCities @@ -923,7 +826,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/CityDto' + $ref: '#/components/schemas/NewCityDtoRequest' required: true responses: "200": @@ -970,37 +873,6 @@ paths: application/json: schema: $ref: '#/components/schemas/ErrorMessage' - put: - tags: - - City - summary: Update assignment of country to a city. - operationId: updateCountryAssignment - parameters: - - name: cityId - in: path - required: true - schema: - type: integer - format: int64 - - name: countryId - in: path - required: true - schema: - type: integer - format: int64 - responses: - "200": - description: OK - content: - application/json: - schema: - $ref: '#/components/schemas/CityDto' - "400": - description: Input data not correct - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorMessage' delete: tags: - City @@ -1028,96 +900,6 @@ paths: application/json: schema: $ref: '#/components/schemas/ErrorMessage' - /api/cities/{cityId}/airports/{airportId}: - post: - tags: - - City - summary: Assign airport to a city. - operationId: assignAirport - parameters: - - name: cityId - in: path - required: true - schema: - type: integer - format: int64 - - name: airportId - in: path - required: true - schema: - type: integer - format: int64 - responses: - "201": - description: OK - content: - application/json: - schema: - $ref: '#/components/schemas/CityDto' - "400": - description: Input data not correct - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorMessage' - put: - tags: - - City - summary: Update assignment of airport to a city. - operationId: updateAirportAssignment - parameters: - - name: cityId - in: path - required: true - schema: - type: integer - format: int64 - - name: airportId - in: path - required: true - schema: - type: integer - format: int64 - responses: - "200": - description: OK - content: - application/json: - schema: - $ref: '#/components/schemas/CityDto' - "400": - description: Input data not correct - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorMessage' - delete: - tags: - - City - summary: Delete assignment of airport to a city. - operationId: deleteAirportAssignment - parameters: - - name: cityId - in: path - required: true - schema: - type: integer - format: int64 - - name: airportId - in: path - required: true - schema: - type: integer - format: int64 - responses: - "204": - description: Deleted - "404": - description: Not Found - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorMessage' /api/airports: get: tags: @@ -1204,7 +986,7 @@ paths: put: tags: - Airport - summary: Update city by id. + summary: Update airport by id. operationId: updateAirport parameters: - name: id @@ -1218,7 +1000,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/AirportDto' + $ref: '#/components/schemas/NewAirportDtoRequest' required: true responses: "200": @@ -1265,37 +1047,6 @@ paths: application/json: schema: $ref: '#/components/schemas/ErrorMessage' - put: - tags: - - Airport - summary: Update assignment of departing flight to an airport. - operationId: updateDepartingFlightAssignment - parameters: - - name: airportId - in: path - required: true - schema: - type: integer - format: int64 - - name: departingFlightId - in: path - required: true - schema: - type: integer - format: int64 - responses: - "200": - description: OK - content: - application/json: - schema: - $ref: '#/components/schemas/AirportDto' - "400": - description: Input data not correct - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorMessage' delete: tags: - Airport @@ -1355,11 +1106,11 @@ paths: application/json: schema: $ref: '#/components/schemas/ErrorMessage' - put: + delete: tags: - Airport - summary: Update assignment of arriving flight to an airport. - operationId: updateArrivingFlightAssignment + summary: Delete assignment of arriving flight to an airport. + operationId: deleteArrivingFlightAssignment parameters: - name: airportId in: path @@ -1374,7 +1125,35 @@ paths: type: integer format: int64 responses: - "200": + "204": + description: Deleted + "404": + description: Not Found + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorMessage' + /api/airports/{airportId}/city/{cityId}: + post: + tags: + - Airport + summary: Assign the city to an airport. + operationId: assignCity + parameters: + - name: airportId + in: path + required: true + schema: + type: integer + format: int64 + - name: cityId + in: path + required: true + schema: + type: integer + format: int64 + responses: + "201": description: OK content: application/json: @@ -1389,8 +1168,8 @@ paths: delete: tags: - Airport - summary: Delete assignment of arriving flight to an airport. - operationId: deleteArrivingFlightAssignment + summary: Delete assignment of the city to an airport. + operationId: deleteCityAssignment parameters: - name: airportId in: path @@ -1398,7 +1177,7 @@ paths: schema: type: integer format: int64 - - name: arrivingFlightId + - name: cityId in: path required: true schema: @@ -1413,9 +1192,32 @@ paths: application/json: schema: $ref: '#/components/schemas/ErrorMessage' + /api/db/seed: + post: + tags: + - Database + summary: Seeds the predefined data database + operationId: seed + responses: + "201": + description: Created + /api/db/clear: + delete: + tags: + - Database + summary: Clears the predefined data database + operationId: clear + responses: + "204": + description: Deleted components: + securitySchemes: + BearerAuth: + type: http + description: "OAuth2 Resource Server, provide a valid access token" + scheme: bearer schemas: - DomainEntity: + DomainEntityDto: title: Domain Entity description: Represents a Domain Entity type: object @@ -1430,8 +1232,6 @@ components: discriminator: propertyName: objectType ErrorMessage: - allOf: - - $ref: '#/components/schemas/DomainEntity' title: Error Message description: Response body for HTML statuses. type: object @@ -1460,7 +1260,7 @@ components: example: /api/stewards/1 StewardDto: allOf: - - $ref: '#/components/schemas/DomainEntity' + - $ref: '#/components/schemas/DomainEntityDto' type: object title: Steward description: Represents a steward on a flight. @@ -1477,11 +1277,12 @@ components: type: string description: last name of a steward example: Doe - flights: + assignedFlightIds: type: array description: flights assigned to a steward items: - $ref: '#/components/schemas/FlightDto' + type: integer + format: int64 NewStewardDtoRequest: type: object title: New Steward Request @@ -1500,7 +1301,7 @@ components: example: Doe FlightDto: allOf: - - $ref: '#/components/schemas/DomainEntity' + - $ref: '#/components/schemas/DomainEntityDto' type: object title: Flight description: Represents a flight. @@ -1508,7 +1309,7 @@ components: - id - departureTime - arrivalTime - - stewards + - stewardIds - airplane - departureAirport - arrivalAirport @@ -1517,23 +1318,27 @@ components: type: string description: time of flight departure format: date-time - example: 2022-12-22T12:04:04.493908908+01:00 + example: 2023-12-22T12:04:04.493908908+01:00 arrivalTime: type: string description: time of flight arrival format: date-time - example: 2022-12-22T12:04:04.493908908+01:00 - stewards: + example: 2023-12-22T12:04:04.493908908+01:00 + assignedStewardIds: type: array description: stewards assigned to a flight items: - $ref: '#/components/schemas/StewardDto' - airplane: - $ref: '#/components/schemas/AirplaneDto' - departureAirport: - $ref: '#/components/schemas/AirportDto' - arrivalAirport: - $ref: '#/components/schemas/AirportDto' + type: integer + format: int64 + airplaneId: + type: integer + format: int64 + departureAirportId: + type: integer + format: int64 + arrivalAirportId: + type: integer + format: int64 NewFlightDtoRequest: type: object title: New Flight Request @@ -1541,20 +1346,26 @@ components: required: - departureTime - arrivalTime + - airplaneId properties: departureTime: type: string description: time of flight departure format: date-time - example: 2022-12-22T12:04:04.493908908+01:00 + example: 2023-12-22T12:04:04.493908908+01:00 arrivalTime: type: string description: time of flight arrival format: date-time - example: 2022-12-22T12:04:04.493908908+01:00 + example: 2023-12-22T12:04:04.493908908+01:00 + airplaneId: + type: integer + format: int64 + description: assigned airplane id + example: 1 AirplaneTypeDto: allOf: - - $ref: '#/components/schemas/DomainEntity' + - $ref: '#/components/schemas/DomainEntityDto' type: object title: Airplane Type description: Represents an Airplane Type. @@ -1566,6 +1377,12 @@ components: type: string description: airplane type name example: Boeing 747 + airplanesIds: + type: array + items: + type: integer + format: int64 + description: assigned airplanes ids NewAirplaneTypeDtoRequest: type: object title: New Airplane Type Request @@ -1579,7 +1396,7 @@ components: example: Boeing 747 AirplaneDto: allOf: - - $ref: '#/components/schemas/DomainEntity' + - $ref: '#/components/schemas/DomainEntityDto' type: object title: Airplane description: Represents an Airplane. @@ -1587,7 +1404,7 @@ components: - id - name - capacity - - type + - typeId properties: name: type: string @@ -1598,8 +1415,11 @@ components: description: airplane seat capacity format: int32 example: 150 - type: - $ref: '#/components/schemas/AirplaneTypeDto' + typeId: + type: integer + format: int64 + description: assigned airplane type's id + example: 1 NewAirplaneDtoRequest: type: object title: New Airplane Request @@ -1607,6 +1427,7 @@ components: required: - name - capacity + - typeId properties: name: type: string @@ -1617,6 +1438,11 @@ components: description: airplane seat capacity format: int32 example: 150 + typeId: + type: integer + format: int64 + description: assigned airplane type's id + example: 1 PageableObject: type: object title: Pageable Object @@ -1681,7 +1507,7 @@ components: type: boolean CountryDto: allOf: - - $ref: '#/components/schemas/DomainEntity' + - $ref: '#/components/schemas/DomainEntityDto' type: object title: Country description: Represents a country. @@ -1693,6 +1519,12 @@ components: type: string description: country name example: Canada + citiesIds: + type: array + items: + type: integer + format: int64 + description: cities assigned to the country NewCountryDtoRequest: type: object title: New Country Request @@ -1706,7 +1538,7 @@ components: example: Canada CityDto: allOf: - - $ref: '#/components/schemas/DomainEntity' + - $ref: '#/components/schemas/DomainEntityDto' type: object title: City description: Represents a City in a Country containing Airports. @@ -1718,12 +1550,16 @@ components: type: string description: city name example: London - country: - $ref: '#/components/schemas/CountryDto' - airports: + countryId: + type: integer + format: int64 + description: id of assigned country, 0 if none assigned yet + airportsIds: type: array items: - $ref: '#/components/schemas/AirportDto' + type: integer + format: int64 + description: ids of assigned airports NewCityDtoRequest: type: object title: New City Request @@ -1753,7 +1589,7 @@ components: example: 2.17403 AirportDto: allOf: - - $ref: '#/components/schemas/DomainEntity' + - $ref: '#/components/schemas/DomainEntityDto' type: object title: Airport description: Represents an Airport. @@ -1761,9 +1597,6 @@ components: - id - name - code - - departingFlights - - arrivingFlights - - city - location properties: name: @@ -1774,18 +1607,24 @@ components: type: string description: airport code in IATA format example: JFK - departingFlights: + location: + $ref: '#/components/schemas/GPSLocationDto' + departingFlightsIds: type: array items: - $ref: '#/components/schemas/FlightDto' - arrivingFlights: + type: integer + format: int64 + description: assigned departing flights ids + arrivingFlightsIds: type: array items: - $ref: '#/components/schemas/FlightDto' - city: - $ref: '#/components/schemas/CityDto' - location: - $ref: '#/components/schemas/GPSLocationDto' + type: integer + format: int64 + description: assigned arriving flights ids + cityId: + type: integer + format: int64 + description: assigned city's id, 0 if none assigned yet NewAirportDtoRequest: type: object title: New Airport Type Request @@ -1793,6 +1632,7 @@ components: required: - name - code + - location properties: name: type: string @@ -1802,6 +1642,8 @@ components: type: string description: airport code in IATA format example: JFK + location: + $ref: '#/components/schemas/GPSLocationDto' responses: SingleStewardDtoResponse: description: Response containing a single Steward. diff --git a/core/pom.xml b/core/pom.xml index 62fe1e1df351da1a959d2fc2b2c93f02c784aa1e..05d16bac11e7e65e359aa8fcb3ad06f483c31db6 100644 --- a/core/pom.xml +++ b/core/pom.xml @@ -21,6 +21,18 @@ <scope>runtime</scope> </dependency> + <!-- Actuator --> + <dependency> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-starter-actuator</artifactId> + </dependency> + + <!-- Prometheus --> + <dependency> + <groupId>io.micrometer</groupId> + <artifactId>micrometer-registry-prometheus</artifactId> + </dependency> + <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> @@ -31,6 +43,11 @@ <artifactId>mapstruct</artifactId> </dependency> + <dependency> + <groupId>org.modelmapper</groupId> + <artifactId>modelmapper</artifactId> + </dependency> + <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> @@ -42,8 +59,8 @@ </dependency> <dependency> - <groupId>jakarta.validation</groupId> - <artifactId>jakarta.validation-api</artifactId> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-starter-validation</artifactId> </dependency> <dependency> @@ -62,13 +79,19 @@ </dependency> <dependency> - <groupId>javax.validation</groupId> - <artifactId>validation-api</artifactId> + <groupId>org.springdoc</groupId> + <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId> </dependency> <dependency> - <groupId>org.springdoc</groupId> - <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-starter-oauth2-resource-server</artifactId> + </dependency> + + <dependency> + <groupId>cz.muni.fi.pa165</groupId> + <artifactId>user-client</artifactId> + <version>1.0-SNAPSHOT</version> </dependency> <!-- for pagination from JPA without actually using JPA --> @@ -77,11 +100,9 @@ <artifactId>spring-data-commons</artifactId> </dependency> - <!-- for testing --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> - <scope>test</scope> </dependency> </dependencies> diff --git a/core/src/main/java/cz/muni/fi/pa165/core/config/AppConfig.java b/core/src/main/java/cz/muni/fi/pa165/core/config/AppConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..53c20c692cb6f905a6fbed337c6b4899cb4af90c --- /dev/null +++ b/core/src/main/java/cz/muni/fi/pa165/core/config/AppConfig.java @@ -0,0 +1,89 @@ +package cz.muni.fi.pa165.core.config; + +import cz.muni.fi.pa165.user.client.Authorities; +import cz.muni.fi.pa165.user.client.UserServiceInterceptionConfigurer; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.http.HttpMethod; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configurers.oauth2.server.resource.OAuth2ResourceServerConfigurer; +import org.springframework.security.web.SecurityFilterChain; + +@Configuration +@Import(UserServiceInterceptionConfigurer.class) +public class AppConfig { + + @Bean + SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { + http.authorizeHttpRequests(x -> x + // swagger: + .requestMatchers("/swagger-ui/**").permitAll() + .requestMatchers("/v3/api-docs/**").permitAll() + .requestMatchers(HttpMethod.GET, "/").permitAll() + .requestMatchers(HttpMethod.GET, "/swagger-ui.html").permitAll() + + // Manager can read everything + .requestMatchers(HttpMethod.GET, "/api/**").hasAuthority(Authorities.MANAGER) + + // Manager can create/update/delete flights + .requestMatchers(HttpMethod.POST, "/api/flights").hasAuthority(Authorities.MANAGER) + .requestMatchers(HttpMethod.PUT, "/api/flights/*").hasAuthority(Authorities.MANAGER) + .requestMatchers(HttpMethod.DELETE, "/api/flights/*").hasAuthority(Authorities.MANAGER) + + // Manager can create/update/delete stewards + .requestMatchers(HttpMethod.POST, "/api/stewards").hasAuthority(Authorities.MANAGER) + .requestMatchers(HttpMethod.PUT, "/api/stewards/*").hasAuthority(Authorities.MANAGER) + .requestMatchers(HttpMethod.DELETE, "/api/stewards/*").hasAuthority(Authorities.MANAGER) + + // Manager can create/update/delete airplanes + .requestMatchers(HttpMethod.POST, "/api/airplanes").hasAuthority(Authorities.MANAGER) + .requestMatchers(HttpMethod.PUT, "/api/airplanes/*").hasAuthority(Authorities.MANAGER) + .requestMatchers(HttpMethod.DELETE, "/api/airplanes/*").hasAuthority(Authorities.MANAGER) + + // Manager can assign flights + .requestMatchers("/api/airports/*/departingFlights/*").hasAuthority(Authorities.MANAGER) + .requestMatchers("/api/airports/*/arrivingFlights/*").hasAuthority(Authorities.MANAGER) + + // Manager can assign stewards + .requestMatchers("/api/stewards/*/flights/*").hasAuthority(Authorities.MANAGER) + + // Administrator can create/update/delete airplane types + .requestMatchers(HttpMethod.GET, "/api/airplaneTypes").hasAuthority(Authorities.ADMINISTRATOR) + .requestMatchers(HttpMethod.GET, "/api/airplaneTypes/*").hasAuthority(Authorities.ADMINISTRATOR) + .requestMatchers(HttpMethod.POST, "/api/airplaneTypes").hasAuthority(Authorities.ADMINISTRATOR) + .requestMatchers(HttpMethod.PUT, "/api/airplaneTypes/*").hasAuthority(Authorities.ADMINISTRATOR) + .requestMatchers(HttpMethod.DELETE, "/api/airplaneTypes/*").hasAuthority(Authorities.ADMINISTRATOR) + + // Administrator can create/update/delete countries + .requestMatchers(HttpMethod.GET, "/api/countries").hasAuthority(Authorities.ADMINISTRATOR) + .requestMatchers(HttpMethod.GET, "/api/countries/*").hasAuthority(Authorities.ADMINISTRATOR) + .requestMatchers(HttpMethod.POST, "/api/countries").hasAuthority(Authorities.ADMINISTRATOR) + .requestMatchers(HttpMethod.PUT, "/api/countries/*").hasAuthority(Authorities.ADMINISTRATOR) + .requestMatchers(HttpMethod.DELETE, "/api/countries/*").hasAuthority(Authorities.ADMINISTRATOR) + + // Administrator can create/update/delete/assign (do everything) with cities + .requestMatchers( "/api/cities").hasAuthority(Authorities.ADMINISTRATOR) + .requestMatchers( "/api/cities/**").hasAuthority(Authorities.ADMINISTRATOR) + + // Administrator read/create/update/delete countries + .requestMatchers(HttpMethod.GET, "/api/countries").hasAuthority(Authorities.ADMINISTRATOR) + .requestMatchers(HttpMethod.GET, "/api/countries/*").hasAuthority(Authorities.ADMINISTRATOR) + .requestMatchers(HttpMethod.POST, "/api/countries").hasAuthority(Authorities.ADMINISTRATOR) + .requestMatchers(HttpMethod.PUT, "/api/countries/*").hasAuthority(Authorities.ADMINISTRATOR) + .requestMatchers(HttpMethod.DELETE, "/api/countries/*").hasAuthority(Authorities.ADMINISTRATOR) + + // Administrator read/create/update/delete airports + .requestMatchers(HttpMethod.GET, "/api/airports").hasAuthority(Authorities.ADMINISTRATOR) + .requestMatchers(HttpMethod.GET, "/api/airports/*").hasAuthority(Authorities.ADMINISTRATOR) + .requestMatchers(HttpMethod.POST, "/api/airports").hasAuthority(Authorities.ADMINISTRATOR) + .requestMatchers(HttpMethod.PUT, "/api/airports/*").hasAuthority(Authorities.ADMINISTRATOR) + .requestMatchers(HttpMethod.DELETE, "/api/airports/*").hasAuthority(Authorities.ADMINISTRATOR) + + // deny everything else + .anyRequest().denyAll() + ) + .oauth2ResourceServer(OAuth2ResourceServerConfigurer::opaqueToken); + return http.build(); + } +} diff --git a/core/src/main/java/cz/muni/fi/pa165/core/data/domain/Airplane.java b/core/src/main/java/cz/muni/fi/pa165/core/data/domain/Airplane.java new file mode 100644 index 0000000000000000000000000000000000000000..c419d22916c8b2013609145c3c790fad996bbf69 --- /dev/null +++ b/core/src/main/java/cz/muni/fi/pa165/core/data/domain/Airplane.java @@ -0,0 +1,46 @@ +package cz.muni.fi.pa165.core.data.domain; + +import cz.muni.fi.pa165.core.data.domain.common.DomainEntity; +import jakarta.persistence.*; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.PositiveOrZero; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.Objects; + +@Entity(name = "airplanes") +@Data +@NoArgsConstructor +public class Airplane extends DomainEntity { + + @NotNull + @NotBlank + @Column(unique = true) + private String name; + + @NotNull + @PositiveOrZero + private Integer capacity; + + @ManyToOne(fetch = FetchType.LAZY) + private AirplaneType type; + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof Airplane airplane)) { + return false; + } + return Objects.equals(getId(), airplane.getId()) && Objects.equals(getName(), airplane.getName()) && + Objects.equals(getCapacity(), airplane.getCapacity()) && Objects.equals(getType(), airplane.getType()); + } + + @Override + public int hashCode() { + return Objects.hash(getId(), getName(), getCapacity(), getType()); + } +} diff --git a/core/src/main/java/cz/muni/fi/pa165/core/data/domain/AirplaneType.java b/core/src/main/java/cz/muni/fi/pa165/core/data/domain/AirplaneType.java new file mode 100644 index 0000000000000000000000000000000000000000..c79756575d1f655f7d4eb22c2074bd82f7e277e9 --- /dev/null +++ b/core/src/main/java/cz/muni/fi/pa165/core/data/domain/AirplaneType.java @@ -0,0 +1,45 @@ +package cz.muni.fi.pa165.core.data.domain; + +import cz.muni.fi.pa165.core.data.domain.common.DomainEntity; +import jakarta.persistence.CascadeType; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.OneToMany; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +@Entity(name = "airplane_types") +@Data +@NoArgsConstructor +public class AirplaneType extends DomainEntity { + + @NotNull + @NotBlank + @Column(unique = true) + private String name; + + @OneToMany(mappedBy = "type", cascade = CascadeType.ALL) + private List<Airplane> airplanes = new ArrayList<>(); + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof AirplaneType airplaneType)) { + return false; + } + return Objects.equals(getName(), airplaneType.getName()); + } + + @Override + public int hashCode() { + return Objects.hash(getName()); + } +} diff --git a/core/src/main/java/cz/muni/fi/pa165/core/data/domain/Airport.java b/core/src/main/java/cz/muni/fi/pa165/core/data/domain/Airport.java new file mode 100644 index 0000000000000000000000000000000000000000..68a2fb8e015c0595ed0b1935b5f6d865b9011482 --- /dev/null +++ b/core/src/main/java/cz/muni/fi/pa165/core/data/domain/Airport.java @@ -0,0 +1,91 @@ +package cz.muni.fi.pa165.core.data.domain; + +import cz.muni.fi.pa165.core.data.domain.common.DomainEntity; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.OneToMany; +import jakarta.persistence.Table; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +@Entity +@Table(name = "airports") +@Data +@NoArgsConstructor +public class Airport extends DomainEntity { + + @NotNull + @NotBlank + @Column(unique = true) + private String name; + + @NotNull + @Column(unique = true) + @Size(min = 3, max = 3) + private String code; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "city_id") + private City city; + + private double latitude; + private double longitude; + + @OneToMany(fetch = FetchType.LAZY, mappedBy = "arrivingAirport") + private List<Flight> arrivingFlights = new ArrayList<>(); + + @OneToMany(fetch = FetchType.LAZY, mappedBy = "departingAirport") + private List<Flight> departingFlights = new ArrayList<>(); + + public void addArrivingFlight(Flight arrivingFlight) { + arrivingFlights.add(arrivingFlight); + } + + public void removeArrivingFlight(Flight arrivingFlight) { + arrivingFlights.remove(arrivingFlight); + } + + public void addDepartingFlight(Flight departingFlight) { + departingFlights.add(departingFlight); + } + + public void removeDepartingFlight(Flight departingFlight) { + departingFlights.remove(departingFlight); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof Airport airport)) { + return false; + } + return getName().equals(airport.getName()) + && getCode().equals(airport.getCode()); + } + + @Override + public int hashCode() { + return Objects.hash(getName(), getCode()); + } + + @Override + public String toString() { + return "Airport{" + + "name='" + name + '\'' + + ", code='" + code + '\'' + + ", city=" + city.getName() + + '}'; + } +} diff --git a/core/src/main/java/cz/muni/fi/pa165/core/data/domain/City.java b/core/src/main/java/cz/muni/fi/pa165/core/data/domain/City.java new file mode 100644 index 0000000000000000000000000000000000000000..e3cc44dd180df600fcd4c911e9765fbbe0527c90 --- /dev/null +++ b/core/src/main/java/cz/muni/fi/pa165/core/data/domain/City.java @@ -0,0 +1,61 @@ +package cz.muni.fi.pa165.core.data.domain; + +import cz.muni.fi.pa165.core.data.domain.common.DomainEntity; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.OneToMany; +import jakarta.persistence.Table; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +@Entity +@Table(name = "cities") +@Data +@NoArgsConstructor +public class City extends DomainEntity { + + @NotNull + @NotBlank + @Column(unique = true) + private String name; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "country_id") + private Country country; + + @OneToMany(fetch = FetchType.LAZY, mappedBy = "city") + private List<Airport> airports = new ArrayList<>(); + + public void addAirport(Airport airport) { + airports.add(airport); + } + + public void removeAirport(Airport airport) { + airports.remove(airport); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof City city)) { + return false; + } + return getName().equals(city.getName()); + } + + @Override + public int hashCode() { + return Objects.hash(getName()); + } +} diff --git a/core/src/main/java/cz/muni/fi/pa165/core/data/domain/Country.java b/core/src/main/java/cz/muni/fi/pa165/core/data/domain/Country.java new file mode 100644 index 0000000000000000000000000000000000000000..c1d18da489a38a55452c34839ba2408197a6cf98 --- /dev/null +++ b/core/src/main/java/cz/muni/fi/pa165/core/data/domain/Country.java @@ -0,0 +1,55 @@ +package cz.muni.fi.pa165.core.data.domain; + +import cz.muni.fi.pa165.core.data.domain.common.DomainEntity; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.OneToMany; +import jakarta.persistence.Table; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +@Entity +@Table(name = "countries") +@Data +@NoArgsConstructor +public class Country extends DomainEntity { + + @NotNull + @NotBlank + @Column(unique = true) + private String name; + + @OneToMany(fetch = FetchType.LAZY, mappedBy = "country") + private List<City> cities = new ArrayList<>(); + + public void addCity(City city) { + cities.add(city); + } + + public void removeCity(City city) { + cities.remove(city); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof Country country)) { + return false; + } + return getName().equals(country.getName()); + } + + @Override + public int hashCode() { + return Objects.hash(getName()); + } +} diff --git a/core/src/main/java/cz/muni/fi/pa165/core/data/domain/Flight.java b/core/src/main/java/cz/muni/fi/pa165/core/data/domain/Flight.java new file mode 100644 index 0000000000000000000000000000000000000000..541481770baac2d614c83fba99224e8ba44749f8 --- /dev/null +++ b/core/src/main/java/cz/muni/fi/pa165/core/data/domain/Flight.java @@ -0,0 +1,61 @@ +package cz.muni.fi.pa165.core.data.domain; + +import cz.muni.fi.pa165.core.data.domain.common.DomainEntity; +import jakarta.persistence.*; +import jakarta.validation.constraints.Future; +import jakarta.validation.constraints.NotNull; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.OffsetDateTime; +import java.util.List; +import java.util.Objects; + +@Entity +@Table(name = "flights") +@Data +@NoArgsConstructor +public class Flight extends DomainEntity { + + @NotNull + @Future + private OffsetDateTime departureTime; + + @NotNull + @Future + private OffsetDateTime arrivalTime; + + @OneToMany(mappedBy = "flight") + private List<FlightSteward> flightStewards; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "departing_airport_id") + private Airport departingAirport; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "arriving_airport_id") + private Airport arrivingAirport; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "airplane_id") + private Airplane airplane; + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof Flight flight)) { + return false; + } + return Objects.equals(getId(), flight.getId()) && + Objects.equals(getDepartureTime(), flight.getDepartureTime()) && + Objects.equals(getArrivalTime(), flight.getArrivalTime()) && + Objects.equals(getFlightStewards(), flight.getFlightStewards()); + } + + @Override + public int hashCode() { + return Objects.hash(getId(), getDepartureTime(), getArrivalTime(), getFlightStewards()); + } +} diff --git a/core/src/main/java/cz/muni/fi/pa165/core/data/domain/FlightSteward.java b/core/src/main/java/cz/muni/fi/pa165/core/data/domain/FlightSteward.java new file mode 100644 index 0000000000000000000000000000000000000000..a873d52b1df4e2d50474de5912a1d10bca477a88 --- /dev/null +++ b/core/src/main/java/cz/muni/fi/pa165/core/data/domain/FlightSteward.java @@ -0,0 +1,46 @@ +package cz.muni.fi.pa165.core.data.domain; + +import cz.muni.fi.pa165.core.data.domain.common.DomainEntity; +import jakarta.persistence.Entity; +import jakarta.persistence.ManyToOne; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.hibernate.annotations.OnDelete; +import org.hibernate.annotations.OnDeleteAction; + +import java.util.Objects; + +/** + * Connection table for flight-steward many-to-many relationship + */ +@Entity(name = "flight_steward") +@Data +@NoArgsConstructor +public class FlightSteward extends DomainEntity { + + @ManyToOne + @OnDelete(action = OnDeleteAction.CASCADE) + private Flight flight; + + @ManyToOne + @OnDelete(action = OnDeleteAction.CASCADE) + private Steward steward; + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof FlightSteward flightSteward)) { + return false; + } + return Objects.equals(getId(), flightSteward.getId()) && + Objects.equals(getFlight(), flightSteward.getFlight()) && + Objects.equals(getSteward(), flightSteward.getSteward()); + } + + @Override + public int hashCode() { + return Objects.hash(getId(), getFlight(), getSteward()); + } +} diff --git a/core/src/main/java/cz/muni/fi/pa165/core/data/domain/Steward.java b/core/src/main/java/cz/muni/fi/pa165/core/data/domain/Steward.java new file mode 100644 index 0000000000000000000000000000000000000000..f742515e9b2badd0991a4973ffaaeab8a7b261f1 --- /dev/null +++ b/core/src/main/java/cz/muni/fi/pa165/core/data/domain/Steward.java @@ -0,0 +1,48 @@ +package cz.muni.fi.pa165.core.data.domain; + +import cz.muni.fi.pa165.core.data.domain.common.DomainEntity; +import jakarta.persistence.*; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; +import java.util.Objects; + +@Entity +@Table(name = "stewards") +@Data +@NoArgsConstructor +public class Steward extends DomainEntity { + + @NotNull + @NotBlank + private String firstName; + + @NotNull + @NotBlank + private String lastName; + + @OneToMany(mappedBy = "steward") + private List<FlightSteward> flightStewards; + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof Steward steward)) { + return false; + } + return Objects.equals(getId(), steward.getId()) && + Objects.equals(getFirstName(), steward.getFirstName()) && + Objects.equals(getLastName(), steward.getLastName()) && + Objects.equals(getFlightStewards(), steward.getFlightStewards()); + } + + @Override + public int hashCode() { + return Objects.hash(getId(), getFirstName(), getLastName(), getFlightStewards()); + } +} diff --git a/core/src/main/java/cz/muni/fi/pa165/core/data/domain/common/DomainEntity.java b/core/src/main/java/cz/muni/fi/pa165/core/data/domain/common/DomainEntity.java new file mode 100644 index 0000000000000000000000000000000000000000..62a5e04a5460b3992c71434aa9c29fbd2016e3bf --- /dev/null +++ b/core/src/main/java/cz/muni/fi/pa165/core/data/domain/common/DomainEntity.java @@ -0,0 +1,16 @@ +package cz.muni.fi.pa165.core.data.domain.common; + +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.MappedSuperclass; +import lombok.Data; + +@MappedSuperclass +@Data +public class DomainEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; +} \ No newline at end of file diff --git a/core/src/main/java/cz/muni/fi/pa165/core/data/repository/airplane/AirplaneRepository.java b/core/src/main/java/cz/muni/fi/pa165/core/data/repository/airplane/AirplaneRepository.java new file mode 100644 index 0000000000000000000000000000000000000000..83ad96aec601a4d5301f932b6d45c1a508bce9f6 --- /dev/null +++ b/core/src/main/java/cz/muni/fi/pa165/core/data/repository/airplane/AirplaneRepository.java @@ -0,0 +1,10 @@ +package cz.muni.fi.pa165.core.data.repository.airplane; + +import cz.muni.fi.pa165.core.data.domain.Airplane; +import cz.muni.fi.pa165.core.data.repository.common.BaseRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface AirplaneRepository extends BaseRepository<Airplane, Long> { + +} diff --git a/core/src/main/java/cz/muni/fi/pa165/core/data/repository/airplanetype/AirplaneTypeRepository.java b/core/src/main/java/cz/muni/fi/pa165/core/data/repository/airplanetype/AirplaneTypeRepository.java new file mode 100644 index 0000000000000000000000000000000000000000..9d08c5c653d117ff898701aca9a9102f5c8a35d5 --- /dev/null +++ b/core/src/main/java/cz/muni/fi/pa165/core/data/repository/airplanetype/AirplaneTypeRepository.java @@ -0,0 +1,13 @@ +package cz.muni.fi.pa165.core.data.repository.airplanetype; + +import cz.muni.fi.pa165.core.data.domain.AirplaneType; +import cz.muni.fi.pa165.core.data.repository.common.BaseRepository; +import org.springframework.stereotype.Repository; + +import java.util.Optional; + +@Repository +public interface AirplaneTypeRepository extends BaseRepository<AirplaneType, Long> { + + Optional<AirplaneType> findByName(String name); +} diff --git a/core/src/main/java/cz/muni/fi/pa165/core/data/repository/airport/AirportRepository.java b/core/src/main/java/cz/muni/fi/pa165/core/data/repository/airport/AirportRepository.java new file mode 100644 index 0000000000000000000000000000000000000000..cfdcef4c8ce1a040c6fa21669cc03d20f62ebaff --- /dev/null +++ b/core/src/main/java/cz/muni/fi/pa165/core/data/repository/airport/AirportRepository.java @@ -0,0 +1,24 @@ +package cz.muni.fi.pa165.core.data.repository.airport; + +import cz.muni.fi.pa165.core.data.domain.Airport; +import cz.muni.fi.pa165.core.data.domain.City; +import cz.muni.fi.pa165.core.data.repository.common.BaseRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; + +import java.util.List; +import java.util.Optional; + +@Repository +public interface AirportRepository extends BaseRepository<Airport, Long> { + + @Query("SELECT a FROM Airport a WHERE a.name = :name") + Optional<Airport> findByName(@Param("name") String name); + + @Query("SELECT a FROM Airport a WHERE a.code = :code") + Optional<Airport> findByCode(@Param("code") String code); + + @Query("SELECT a FROM Airport a WHERE a.city = :city") + List<Airport> findByCity(@Param("city") City city); +} diff --git a/core/src/main/java/cz/muni/fi/pa165/core/data/repository/city/CityRepository.java b/core/src/main/java/cz/muni/fi/pa165/core/data/repository/city/CityRepository.java new file mode 100644 index 0000000000000000000000000000000000000000..5dca32556bc6a8af19e2c4ce8a32107d9c63be6f --- /dev/null +++ b/core/src/main/java/cz/muni/fi/pa165/core/data/repository/city/CityRepository.java @@ -0,0 +1,16 @@ +package cz.muni.fi.pa165.core.data.repository.city; + +import cz.muni.fi.pa165.core.data.domain.City; +import cz.muni.fi.pa165.core.data.repository.common.BaseRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; + +import java.util.Optional; + +@Repository +public interface CityRepository extends BaseRepository<City, Long> { + + @Query("SELECT c FROM City c WHERE c.name = :name") + Optional<City> findByName(@Param("name") String name); +} diff --git a/core/src/main/java/cz/muni/fi/pa165/core/data/repository/common/BaseRepository.java b/core/src/main/java/cz/muni/fi/pa165/core/data/repository/common/BaseRepository.java new file mode 100644 index 0000000000000000000000000000000000000000..59c3038b4e61d54c6808f9470f98060ac597f60c --- /dev/null +++ b/core/src/main/java/cz/muni/fi/pa165/core/data/repository/common/BaseRepository.java @@ -0,0 +1,44 @@ +package cz.muni.fi.pa165.core.data.repository.common; + +import cz.muni.fi.pa165.core.data.domain.common.DomainEntity; +import org.springframework.data.repository.CrudRepository; +import org.springframework.stereotype.Repository; +import org.springframework.validation.annotation.Validated; + +import java.util.Optional; + +/** + * BaseRepository for common CRUD operations + * + * @param <E> Entity + * @param <K> Key + * @author martinslovik + */ +@Repository +@Validated +public interface BaseRepository<E extends DomainEntity, K> extends CrudRepository<E, K> { + + <S extends E> S save(S entity); + + <S extends E> Iterable<S> saveAll(Iterable<S> entities); + + Optional<E> findById(K id); + + boolean existsById(K id); + + Iterable<E> findAll(); + + Iterable<E> findAllById(Iterable<K> ids); + + long count(); + + void deleteById(K id); + + void delete(E entity); + + void deleteAllById(Iterable<? extends K> ids); + + void deleteAll(Iterable<? extends E> entities); + + void deleteAll(); +} diff --git a/core/src/main/java/cz/muni/fi/pa165/core/data/repository/country/CountryRepository.java b/core/src/main/java/cz/muni/fi/pa165/core/data/repository/country/CountryRepository.java new file mode 100644 index 0000000000000000000000000000000000000000..011541c54631c02a8c9f2bd5af7bc3b41b49eb5f --- /dev/null +++ b/core/src/main/java/cz/muni/fi/pa165/core/data/repository/country/CountryRepository.java @@ -0,0 +1,16 @@ +package cz.muni.fi.pa165.core.data.repository.country; + +import cz.muni.fi.pa165.core.data.domain.Country; +import cz.muni.fi.pa165.core.data.repository.common.BaseRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; + +import java.util.Optional; + +@Repository +public interface CountryRepository extends BaseRepository<Country, Long> { + + @Query("SELECT c FROM Country c WHERE c.name = :name") + Optional<Country> findByName(@Param("name") String name); +} diff --git a/core/src/main/java/cz/muni/fi/pa165/core/data/repository/flight/FlightRepository.java b/core/src/main/java/cz/muni/fi/pa165/core/data/repository/flight/FlightRepository.java new file mode 100644 index 0000000000000000000000000000000000000000..66d0f4d27b18077a8a5528a892dc4bce18c1a0e4 --- /dev/null +++ b/core/src/main/java/cz/muni/fi/pa165/core/data/repository/flight/FlightRepository.java @@ -0,0 +1,21 @@ +package cz.muni.fi.pa165.core.data.repository.flight; + +import cz.muni.fi.pa165.core.data.domain.Flight; +import cz.muni.fi.pa165.core.data.repository.common.BaseRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; + +import java.util.Optional; + +@Repository +public interface FlightRepository extends BaseRepository<Flight, Long> { + + /** + * Returns Flight entity with eagerly fetched Stewards + * @param id flightId + * @return Optional<Flight> + */ + @Query("SELECT f FROM Flight f JOIN FETCH f.flightStewards fs WHERE f.id = :id") + Optional<Flight> findByIdWithStewards(@Param("id") Long id); +} diff --git a/core/src/main/java/cz/muni/fi/pa165/core/data/repository/flightsteward/FlightStewardRepository.java b/core/src/main/java/cz/muni/fi/pa165/core/data/repository/flightsteward/FlightStewardRepository.java new file mode 100644 index 0000000000000000000000000000000000000000..3d9990713401e213b22aaebfb94847bfd72806cb --- /dev/null +++ b/core/src/main/java/cz/muni/fi/pa165/core/data/repository/flightsteward/FlightStewardRepository.java @@ -0,0 +1,15 @@ +package cz.muni.fi.pa165.core.data.repository.flightsteward; + +import cz.muni.fi.pa165.core.data.domain.Flight; +import cz.muni.fi.pa165.core.data.domain.FlightSteward; +import cz.muni.fi.pa165.core.data.domain.Steward; +import cz.muni.fi.pa165.core.data.repository.common.BaseRepository; +import org.springframework.stereotype.Repository; + +import java.util.Optional; + +@Repository +public interface FlightStewardRepository extends BaseRepository<FlightSteward, Long> { + + Optional<FlightSteward> findByStewardAndFlight(Steward steward, Flight flight); +} diff --git a/core/src/main/java/cz/muni/fi/pa165/core/data/repository/steward/StewardRepository.java b/core/src/main/java/cz/muni/fi/pa165/core/data/repository/steward/StewardRepository.java new file mode 100644 index 0000000000000000000000000000000000000000..78064e0b0cafba567a1dacff1f3eedf66161f7ce --- /dev/null +++ b/core/src/main/java/cz/muni/fi/pa165/core/data/repository/steward/StewardRepository.java @@ -0,0 +1,16 @@ +package cz.muni.fi.pa165.core.data.repository.steward; + +import cz.muni.fi.pa165.core.data.domain.Steward; +import cz.muni.fi.pa165.core.data.repository.common.BaseRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; + +import java.util.Optional; + +@Repository +public interface StewardRepository extends BaseRepository<Steward, Long> { + + @Query("SELECT s FROM Steward s JOIN FETCH s.flightStewards fs WHERE s.id = :id") + Optional<Steward> findByIdWithFlights(@Param("id") Long id); +} diff --git a/core/src/main/java/cz/muni/fi/pa165/core/data/seed/DatabaseInitializer.java b/core/src/main/java/cz/muni/fi/pa165/core/data/seed/DatabaseInitializer.java new file mode 100644 index 0000000000000000000000000000000000000000..be5e643c5d39895f7c9c2bbf341411f966e9f006 --- /dev/null +++ b/core/src/main/java/cz/muni/fi/pa165/core/data/seed/DatabaseInitializer.java @@ -0,0 +1,174 @@ +package cz.muni.fi.pa165.core.data.seed; + +import cz.muni.fi.pa165.core.data.domain.*; +import cz.muni.fi.pa165.core.data.repository.airplane.AirplaneRepository; +import cz.muni.fi.pa165.core.data.repository.airplanetype.AirplaneTypeRepository; +import cz.muni.fi.pa165.core.data.repository.airport.AirportRepository; +import cz.muni.fi.pa165.core.data.repository.city.CityRepository; +import cz.muni.fi.pa165.core.data.repository.common.BaseRepository; +import cz.muni.fi.pa165.core.data.repository.country.CountryRepository; +import cz.muni.fi.pa165.core.data.repository.flight.FlightRepository; +import cz.muni.fi.pa165.core.data.repository.flightsteward.FlightStewardRepository; +import cz.muni.fi.pa165.core.data.repository.steward.StewardRepository; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.time.OffsetDateTime; +import java.util.ArrayList; +import java.util.List; + +@Component +public class DatabaseInitializer { + + private final List<BaseRepository<?, ?>> repositories = new ArrayList<>(); + + private final AirplaneRepository airplaneRepository; + private final AirplaneTypeRepository airplaneTypeRepository; + private final AirportRepository airportRepository; + private final CityRepository cityRepository; + private final CountryRepository countryRepository; + private final FlightRepository flightRepository; + private final FlightStewardRepository flightStewardRepository; + private final StewardRepository stewardRepository; + + @Autowired + public DatabaseInitializer(AirplaneRepository airplaneRepository, + AirplaneTypeRepository airplaneTypeRepository, + AirportRepository airportRepository, + CityRepository cityRepository, + CountryRepository countryRepository, + FlightRepository flightRepository, + FlightStewardRepository flightStewardRepository, + StewardRepository stewardRepository) { + this.airplaneRepository = airplaneRepository; + this.airplaneTypeRepository = airplaneTypeRepository; + this.airportRepository = airportRepository; + this.cityRepository = cityRepository; + this.countryRepository = countryRepository; + this.flightRepository = flightRepository; + this.flightStewardRepository = flightStewardRepository; + this.stewardRepository = stewardRepository; + + repositories.add(flightRepository); + repositories.add(flightStewardRepository); + repositories.add(stewardRepository); + repositories.add(airportRepository); + repositories.add(cityRepository); + repositories.add(countryRepository); + repositories.add(airplaneTypeRepository); + repositories.add(airplaneRepository); + } + + public void clear() { + repositories.forEach(BaseRepository::deleteAll); + } + + public void seed() { + clear(); + + var boeing777 = createAirplaneType("Boeing 777"); + airplaneTypeRepository.save(boeing777); + + var freddyTheAirplane = createAirplane("Freddy The Airplane", 100, boeing777); + airplaneRepository.save(freddyTheAirplane); + + var flight1 = createFlight(OffsetDateTime.parse("2023-12-22T12:04:04.493908908+01:00"), + OffsetDateTime.parse("2023-12-22T12:05:05.493908908+01:00"), freddyTheAirplane); + flightRepository.save(flight1); + + var steward1 = createSteward("John", "Doe"); + stewardRepository.save(steward1); + + var steward2 = createSteward("Jane", "Doe"); + stewardRepository.save(steward2); + + var flight1Steward1 = assignFlightToSteward(flight1, steward1); + flightStewardRepository.save(flight1Steward1); + + var flight1Steward2 = assignFlightToSteward(flight1, steward2); + flightStewardRepository.save(flight1Steward2); + + var germany = createCountry("Germany"); + countryRepository.save(germany); + + var france = createCountry("France"); + countryRepository.save(france); + + var berlin = createCity("Berlin"); + berlin.setCountry(germany); + cityRepository.save(berlin); + + var paris = createCity("Paris"); + paris.setCountry(france); + cityRepository.save(paris); + + var ber = createAirport("Berlin Brandenburg Airport", "BER", 52.3666666667, 13.5033333333); + ber.setCity(berlin); + airportRepository.save(ber); + flight1.setDepartingAirport(ber); + flightRepository.save(flight1); + + var cdg = createAirport("Paris Charles de Gaulle airport", "CDG", 49.0083899664, 2.53844117956); + cdg.setCity(paris); + airportRepository.save(cdg); + flight1.setArrivingAirport(cdg); + flightRepository.save(flight1); + } + + private Country createCountry(String name) { + var country = new Country(); + country.setName(name); + return country; + } + + private City createCity(String name) { + var city = new City(); + city.setName(name); + return city; + } + + private AirplaneType createAirplaneType(String name) { + var airplaneType = new AirplaneType(); + airplaneType.setName(name); + return airplaneType; + } + + private Airplane createAirplane(String name, Integer capacity, AirplaneType airplaneType) { + var airplane = new Airplane(); + airplane.setName(name); + airplane.setCapacity(capacity); + airplane.setType(airplaneType); + return airplane; + } + + private Flight createFlight(OffsetDateTime departureTime, OffsetDateTime arrivalTime, Airplane airplane) { + var flight = new Flight(); + flight.setDepartureTime(departureTime); + flight.setArrivalTime(arrivalTime); + flight.setAirplane(airplane); + return flight; + } + + private Steward createSteward(String firstName, String lastName) { + var steward = new Steward(); + steward.setFirstName(firstName); + steward.setLastName(lastName); + return steward; + } + + private FlightSteward assignFlightToSteward(Flight flight, Steward steward) { + var flightSteward = new FlightSteward(); + flightSteward.setFlight(flight); + flightSteward.setSteward(steward); + return flightSteward; + } + + private Airport createAirport(String name, String code, double latitude, double longitude) { + var airport = new Airport(); + airport.setName(name); + airport.setCode(code); + airport.setLatitude(latitude); + airport.setLongitude(longitude); + return airport; + } +} diff --git a/core/src/main/java/cz/muni/fi/pa165/core/exceptions/ConstraintValidationAdvice.java b/core/src/main/java/cz/muni/fi/pa165/core/exceptions/ConstraintValidationAdvice.java new file mode 100644 index 0000000000000000000000000000000000000000..87a4a57a437866a4909d202a552db0f039ff9a12 --- /dev/null +++ b/core/src/main/java/cz/muni/fi/pa165/core/exceptions/ConstraintValidationAdvice.java @@ -0,0 +1,28 @@ +package cz.muni.fi.pa165.core.exceptions; + +import cz.muni.fi.pa165.core.model.ErrorMessage; +import jakarta.validation.ConstraintViolationException; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.servlet.support.ServletUriComponentsBuilder; + +import java.time.OffsetDateTime; + +@ControllerAdvice +public class ConstraintValidationAdvice { + + @ExceptionHandler(ConstraintViolationException.class) + @ResponseStatus(HttpStatus.UNPROCESSABLE_ENTITY) + ResponseEntity<ErrorMessage> handleConstraintValidationException(ConstraintViolationException ex) { + var errorMessage = new ErrorMessage(); + errorMessage.setTimestamp(OffsetDateTime.now()); + errorMessage.setStatus(HttpStatus.UNPROCESSABLE_ENTITY.value()); + errorMessage.setError(HttpStatus.UNPROCESSABLE_ENTITY.getReasonPhrase()); + errorMessage.setMessage(ex.getMessage()); + errorMessage.setPath(ServletUriComponentsBuilder.fromCurrentRequestUri().toUriString()); + return new ResponseEntity<>(errorMessage, HttpStatus.UNPROCESSABLE_ENTITY); + } +} diff --git a/core/src/main/java/cz/muni/fi/pa165/core/exceptions/DataIntegrityValidationAdvice.java b/core/src/main/java/cz/muni/fi/pa165/core/exceptions/DataIntegrityValidationAdvice.java new file mode 100644 index 0000000000000000000000000000000000000000..08385ecbbdd6d23550f0f841494ee49d6d77d989 --- /dev/null +++ b/core/src/main/java/cz/muni/fi/pa165/core/exceptions/DataIntegrityValidationAdvice.java @@ -0,0 +1,32 @@ +package cz.muni.fi.pa165.core.exceptions; + +import cz.muni.fi.pa165.core.model.ErrorMessage; +import org.springframework.dao.DataIntegrityViolationException; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.servlet.support.ServletUriComponentsBuilder; + +import java.time.OffsetDateTime; + +@ControllerAdvice +public class DataIntegrityValidationAdvice { + + @ExceptionHandler(DataIntegrityViolationException.class) + @ResponseStatus(HttpStatus.CONFLICT) + ResponseEntity<ErrorMessage> handleDataIntegrityViolationException(DataIntegrityViolationException ex) { + var errorMessage = new ErrorMessage(); + errorMessage.setTimestamp(OffsetDateTime.now()); + errorMessage.setStatus(HttpStatus.CONFLICT.value()); + errorMessage.setError(HttpStatus.CONFLICT.getReasonPhrase()); + /* + Custom exception message for security reasons to prevent showing failed insert SQL statement + to the Client and therefore revealing our DB tables. + */ + errorMessage.setMessage("fields must be unique."); + errorMessage.setPath(ServletUriComponentsBuilder.fromCurrentRequestUri().toUriString()); + return new ResponseEntity<>(errorMessage, HttpStatus.CONFLICT); + } +} diff --git a/core/src/main/java/cz/muni/fi/pa165/core/exceptions/InvalidGpsLocationAdvice.java b/core/src/main/java/cz/muni/fi/pa165/core/exceptions/InvalidGpsLocationAdvice.java new file mode 100644 index 0000000000000000000000000000000000000000..3c80af99f70582c7f7a0b82e567685d64cbc6c6b --- /dev/null +++ b/core/src/main/java/cz/muni/fi/pa165/core/exceptions/InvalidGpsLocationAdvice.java @@ -0,0 +1,27 @@ +package cz.muni.fi.pa165.core.exceptions; + +import cz.muni.fi.pa165.core.model.ErrorMessage; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.servlet.support.ServletUriComponentsBuilder; + +import java.time.OffsetDateTime; + +@ControllerAdvice +public class InvalidGpsLocationAdvice { + + @ExceptionHandler(InvalidGpsLocationException.class) + @ResponseStatus(HttpStatus.UNPROCESSABLE_ENTITY) + ResponseEntity<ErrorMessage> handleInvalidGpsLocationException(InvalidGpsLocationException ex) { + var errorMessage = new ErrorMessage(); + errorMessage.setTimestamp(OffsetDateTime.now()); + errorMessage.setStatus(HttpStatus.UNPROCESSABLE_ENTITY.value()); + errorMessage.setError(HttpStatus.UNPROCESSABLE_ENTITY.getReasonPhrase()); + errorMessage.setMessage(ex.getMessage()); + errorMessage.setPath(ServletUriComponentsBuilder.fromCurrentRequestUri().toUriString()); + return new ResponseEntity<>(errorMessage, HttpStatus.UNPROCESSABLE_ENTITY); + } +} diff --git a/core/src/main/java/cz/muni/fi/pa165/core/exceptions/InvalidGpsLocationException.java b/core/src/main/java/cz/muni/fi/pa165/core/exceptions/InvalidGpsLocationException.java new file mode 100644 index 0000000000000000000000000000000000000000..302b42e483c6af1c7cb57e60ee74bc1d460f1b43 --- /dev/null +++ b/core/src/main/java/cz/muni/fi/pa165/core/exceptions/InvalidGpsLocationException.java @@ -0,0 +1,11 @@ +package cz.muni.fi.pa165.core.exceptions; + +/** + * Thrown in case of invalid GPS location, e.g. (-120.3, 42.42). + */ +public class InvalidGpsLocationException extends RuntimeException { + + public InvalidGpsLocationException(String message) { + super(message); + } +} diff --git a/core/src/main/java/cz/muni/fi/pa165/core/exceptions/ResourceNotFoundAdvice.java b/core/src/main/java/cz/muni/fi/pa165/core/exceptions/ResourceNotFoundAdvice.java new file mode 100644 index 0000000000000000000000000000000000000000..52199bdecfb9ebae72dd69ecc13a7b6b572dabef --- /dev/null +++ b/core/src/main/java/cz/muni/fi/pa165/core/exceptions/ResourceNotFoundAdvice.java @@ -0,0 +1,27 @@ +package cz.muni.fi.pa165.core.exceptions; + +import cz.muni.fi.pa165.core.model.ErrorMessage; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.servlet.support.ServletUriComponentsBuilder; + +import java.time.OffsetDateTime; + +@ControllerAdvice +public class ResourceNotFoundAdvice { + + @ExceptionHandler(ResourceNotFoundException.class) + @ResponseStatus(HttpStatus.NOT_FOUND) + ResponseEntity<ErrorMessage> handleResourceNotFoundException(ResourceNotFoundException ex) { + ErrorMessage errorMessage = new ErrorMessage(); + errorMessage.setTimestamp(OffsetDateTime.now()); + errorMessage.setStatus(HttpStatus.NOT_FOUND.value()); + errorMessage.setError(HttpStatus.NOT_FOUND.getReasonPhrase()); + errorMessage.setMessage(ex.getMessage()); + errorMessage.setPath(ServletUriComponentsBuilder.fromCurrentRequestUri().toUriString()); + return new ResponseEntity<>(errorMessage, HttpStatus.NOT_FOUND); + } +} diff --git a/core/src/main/java/cz/muni/fi/pa165/core/exceptions/ResourceNotFoundException.java b/core/src/main/java/cz/muni/fi/pa165/core/exceptions/ResourceNotFoundException.java new file mode 100644 index 0000000000000000000000000000000000000000..a226a1b647cf845048c93afea4b37785aca05dc5 --- /dev/null +++ b/core/src/main/java/cz/muni/fi/pa165/core/exceptions/ResourceNotFoundException.java @@ -0,0 +1,25 @@ +package cz.muni.fi.pa165.core.exceptions; + +public class ResourceNotFoundException extends RuntimeException { + + public ResourceNotFoundException() { + super("Resource Not Found"); + } + + public ResourceNotFoundException(String message) { + super(message); + } + + public ResourceNotFoundException(String message, Throwable cause) { + super(message, cause); + } + + public ResourceNotFoundException(Throwable cause) { + super(cause); + } + + public ResourceNotFoundException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) { + super(message, cause, enableSuppression, writableStackTrace); + } +} + diff --git a/core/src/main/java/cz/muni/fi/pa165/core/facade/airplane/AirplaneFacade.java b/core/src/main/java/cz/muni/fi/pa165/core/facade/airplane/AirplaneFacade.java new file mode 100644 index 0000000000000000000000000000000000000000..db3abbef5f43662f21c8f4bcedda69d339555660 --- /dev/null +++ b/core/src/main/java/cz/muni/fi/pa165/core/facade/airplane/AirplaneFacade.java @@ -0,0 +1,26 @@ +package cz.muni.fi.pa165.core.facade.airplane; + +import cz.muni.fi.pa165.core.model.AirplaneDto; +import cz.muni.fi.pa165.core.model.NewAirplaneDtoRequest; + +import java.util.List; +import java.util.Optional; + +/** + * @param <K> Key + * @author martinslovik + */ +public interface AirplaneFacade<K> { + + AirplaneDto save(NewAirplaneDtoRequest newAirplaneDtoRequest); + + Optional<AirplaneDto> findById(K id); + + List<AirplaneDto> findAll(); + + void deleteById(K id); + + void deleteAll(); + + AirplaneDto update(Long id, NewAirplaneDtoRequest newAirplaneDtoRequest); +} diff --git a/core/src/main/java/cz/muni/fi/pa165/core/facade/airplane/AirplaneFacadeImpl.java b/core/src/main/java/cz/muni/fi/pa165/core/facade/airplane/AirplaneFacadeImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..3cef2e7d3bc2821caeb8080a483d9d44f1c61f2e --- /dev/null +++ b/core/src/main/java/cz/muni/fi/pa165/core/facade/airplane/AirplaneFacadeImpl.java @@ -0,0 +1,73 @@ +package cz.muni.fi.pa165.core.facade.airplane; + +import cz.muni.fi.pa165.core.exceptions.ResourceNotFoundException; +import cz.muni.fi.pa165.core.mapper.AirplaneMapper; +import cz.muni.fi.pa165.core.model.AirplaneDto; +import cz.muni.fi.pa165.core.model.NewAirplaneDtoRequest; +import cz.muni.fi.pa165.core.service.airplane.AirplaneService; +import cz.muni.fi.pa165.core.service.airplanetype.AirplaneTypeService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.List; +import java.util.Optional; + +@Service +public class AirplaneFacadeImpl implements AirplaneFacade<Long> { + + private final AirplaneService airplaneService; + private final AirplaneTypeService airplaneTypeService; + private final AirplaneMapper airplaneMapper; + + @Autowired + public AirplaneFacadeImpl(AirplaneService airplaneService, AirplaneTypeService airplaneTypeService, AirplaneMapper airplaneMapper) { + this.airplaneService = airplaneService; + this.airplaneTypeService = airplaneTypeService; + this.airplaneMapper = airplaneMapper; + } + + @Override + public AirplaneDto save(NewAirplaneDtoRequest newAirplaneDtoRequest) { + var airplaneType = airplaneTypeService.findById(newAirplaneDtoRequest.getTypeId()) + .orElseThrow(ResourceNotFoundException::new); + var entityToSave = airplaneMapper.toEntityFromNewRequest(newAirplaneDtoRequest); + entityToSave.setType(airplaneType); + var savedEntity = airplaneService.save(entityToSave); + return airplaneMapper.toDto(savedEntity); + } + + @Override + public Optional<AirplaneDto> findById(Long id) { + var foundEntity = airplaneService.findById(id) + .orElseThrow(ResourceNotFoundException::new); + + return Optional.ofNullable(airplaneMapper.toDto(foundEntity)); + } + + @Override + public List<AirplaneDto> findAll() { + var entities = airplaneService.findAll(); + return entities.stream() + .map(airplaneMapper::toDto) + .toList(); + } + + @Override + public void deleteById(Long id) { + airplaneService.deleteById(id); + } + + @Override + public void deleteAll() { + airplaneService.deleteAll(); + } + + @Override + public AirplaneDto update(Long id, NewAirplaneDtoRequest newAirplaneDtoRequest) { + var newAirplaneEntity = airplaneMapper.toEntityFromNewRequest(newAirplaneDtoRequest); + + var savedEntity = airplaneService.update(id, newAirplaneEntity); + + return airplaneMapper.toDto(savedEntity); + } +} diff --git a/core/src/main/java/cz/muni/fi/pa165/core/facade/airplanetype/AirplaneTypeFacade.java b/core/src/main/java/cz/muni/fi/pa165/core/facade/airplanetype/AirplaneTypeFacade.java new file mode 100644 index 0000000000000000000000000000000000000000..2f01c4dcd2a3caa0ecf5dff20b9558faadaab375 --- /dev/null +++ b/core/src/main/java/cz/muni/fi/pa165/core/facade/airplanetype/AirplaneTypeFacade.java @@ -0,0 +1,26 @@ +package cz.muni.fi.pa165.core.facade.airplanetype; + +import cz.muni.fi.pa165.core.model.AirplaneTypeDto; +import cz.muni.fi.pa165.core.model.NewAirplaneTypeDtoRequest; + +import java.util.List; +import java.util.Optional; + +/** + * @param <K> Key + * @author martinslovik + */ +public interface AirplaneTypeFacade<K> { + + AirplaneTypeDto save(NewAirplaneTypeDtoRequest newAirplaneTypeDtoRequest); + + Optional<AirplaneTypeDto> findById(K id); + + List<AirplaneTypeDto> findAll(); + + void deleteById(K id); + + void deleteAll(); + + AirplaneTypeDto update(K id, NewAirplaneTypeDtoRequest newAirplaneTypeDtoRequest); +} \ No newline at end of file diff --git a/core/src/main/java/cz/muni/fi/pa165/core/facade/airplanetype/AirplaneTypeFacadeImpl.java b/core/src/main/java/cz/muni/fi/pa165/core/facade/airplanetype/AirplaneTypeFacadeImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..2e835e562d283fa6a02281d7ce96c4ead7e38ca1 --- /dev/null +++ b/core/src/main/java/cz/muni/fi/pa165/core/facade/airplanetype/AirplaneTypeFacadeImpl.java @@ -0,0 +1,66 @@ +package cz.muni.fi.pa165.core.facade.airplanetype; + +import cz.muni.fi.pa165.core.exceptions.ResourceNotFoundException; +import cz.muni.fi.pa165.core.mapper.AirplaneTypeMapper; +import cz.muni.fi.pa165.core.model.AirplaneTypeDto; +import cz.muni.fi.pa165.core.model.NewAirplaneTypeDtoRequest; +import cz.muni.fi.pa165.core.service.airplanetype.AirplaneTypeService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.List; +import java.util.Optional; + +@Service +public class AirplaneTypeFacadeImpl implements AirplaneTypeFacade<Long> { + + private final AirplaneTypeService airplaneTypeService; + private final AirplaneTypeMapper airplaneTypeMapper; + + @Autowired + public AirplaneTypeFacadeImpl(AirplaneTypeService airplaneTypeService, AirplaneTypeMapper airplaneTypeMapper) { + this.airplaneTypeService = airplaneTypeService; + this.airplaneTypeMapper = airplaneTypeMapper; + } + + @Override + public AirplaneTypeDto save(NewAirplaneTypeDtoRequest newAirplaneTypeDtoRequest) { + var entityToSave = airplaneTypeMapper.toEntityFromNewRequest(newAirplaneTypeDtoRequest); + var savedEntity = airplaneTypeService.save(entityToSave); + return airplaneTypeMapper.toDto(savedEntity); + } + + @Override + public Optional<AirplaneTypeDto> findById(Long id) { + var foundEntity = airplaneTypeService.findById(id) + .orElseThrow(ResourceNotFoundException::new); + + return Optional.ofNullable(airplaneTypeMapper.toDto(foundEntity)); + } + + @Override + public List<AirplaneTypeDto> findAll() { + var entities = airplaneTypeService.findAll(); + return entities.stream() + .map(airplaneTypeMapper::toDto) + .toList(); + } + + @Override + public void deleteById(Long id) { + airplaneTypeService.deleteById(id); + } + + @Override + public void deleteAll() { + airplaneTypeService.deleteAll(); + } + + @Override + public AirplaneTypeDto update(Long id, NewAirplaneTypeDtoRequest newAirplaneTypeDtoRequest) { + var newAirplaneTypeEntity = airplaneTypeMapper.toEntityFromNewRequest(newAirplaneTypeDtoRequest); + + return airplaneTypeMapper.toDto(airplaneTypeService.update(id, newAirplaneTypeEntity)); + } + +} diff --git a/core/src/main/java/cz/muni/fi/pa165/core/facade/airport/AirportFacade.java b/core/src/main/java/cz/muni/fi/pa165/core/facade/airport/AirportFacade.java new file mode 100644 index 0000000000000000000000000000000000000000..71a6ddd8d63d29dae04020e63fc517525b0e9eab --- /dev/null +++ b/core/src/main/java/cz/muni/fi/pa165/core/facade/airport/AirportFacade.java @@ -0,0 +1,43 @@ +package cz.muni.fi.pa165.core.facade.airport; + +import cz.muni.fi.pa165.core.model.AirportDto; +import cz.muni.fi.pa165.core.model.NewAirportDtoRequest; + +import java.util.List; +import java.util.Optional; + +/** + * @param <K> Key + */ +public interface AirportFacade<K> { + + AirportDto save(NewAirportDtoRequest newAirportDtoRequest); + + Optional<AirportDto> findById(K id); + + List<AirportDto> findAll(); + + void deleteById(K id); + + void deleteAll(); + + AirportDto update(K id, NewAirportDtoRequest newAirportDtoRequest); + + Optional<AirportDto> findByName(String name); + + Optional<AirportDto> findByCode(String code); + + List<AirportDto> findByCity(K cityId); + + AirportDto addArrivingFlightAssignment(K airportId, K flightId); + + AirportDto deleteArrivingFlightAssignment(K airportId, K flightId); + + AirportDto addDepartingFlightAssignment(K airportId, K flightId); + + AirportDto deleteDepartingFlightAssignment(K airportId, K flightId); + + AirportDto addCityAssignment(K airportId, K cityId); + + AirportDto deleteCityAssignment(K airportId, K cityId); +} diff --git a/core/src/main/java/cz/muni/fi/pa165/core/facade/airport/AirportFacadeImpl.java b/core/src/main/java/cz/muni/fi/pa165/core/facade/airport/AirportFacadeImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..323ce754ef0d38905b02a3045e4c14bc3b6cd5e5 --- /dev/null +++ b/core/src/main/java/cz/muni/fi/pa165/core/facade/airport/AirportFacadeImpl.java @@ -0,0 +1,204 @@ +package cz.muni.fi.pa165.core.facade.airport; + +import cz.muni.fi.pa165.core.data.domain.Airport; +import cz.muni.fi.pa165.core.data.domain.City; +import cz.muni.fi.pa165.core.data.domain.Flight; +import cz.muni.fi.pa165.core.exceptions.ResourceNotFoundException; +import cz.muni.fi.pa165.core.mapper.AirportMapper; +import cz.muni.fi.pa165.core.model.AirportDto; +import cz.muni.fi.pa165.core.model.NewAirportDtoRequest; +import cz.muni.fi.pa165.core.service.airport.AirportService; +import cz.muni.fi.pa165.core.service.city.CityService; +import cz.muni.fi.pa165.core.service.flight.FlightService; +import cz.muni.fi.pa165.core.validation.GpsLocationValidator; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.List; +import java.util.Optional; + +@Service +public class AirportFacadeImpl implements AirportFacade<Long> { + + private final AirportService airportService; + private final FlightService flightService; + private final CityService cityService; + private final AirportMapper airportMapper; + private final GpsLocationValidator gpsLocationValidator; + + @Autowired + public AirportFacadeImpl(AirportService airportService, FlightService flightService, CityService cityService, + AirportMapper airportMapper, GpsLocationValidator gpsLocationValidator) { + this.airportService = airportService; + this.flightService = flightService; + this.cityService = cityService; + this.airportMapper = airportMapper; + this.gpsLocationValidator = gpsLocationValidator; + } + + @Override + public AirportDto save(NewAirportDtoRequest newAirportDtoRequest) { + gpsLocationValidator.validate(newAirportDtoRequest.getLocation().getLatitude(), + newAirportDtoRequest.getLocation().getLongitude()); + + Airport entityToSave = airportMapper.toEntityFromNewRequest(newAirportDtoRequest); + Airport savedEntity = airportService.save(entityToSave); + return airportMapper.toDto(savedEntity); + } + + @Override + public Optional<AirportDto> findById(Long id) { + Airport foundEntity = airportService.findById(id) + .orElseThrow(ResourceNotFoundException::new); + return Optional.of(airportMapper.toDto(foundEntity)); + } + + @Override + public List<AirportDto> findAll() { + List<Airport> foundEntities = airportService.findAll(); + return foundEntities.stream() + .map(airportMapper::toDto) + .toList(); + } + + @Override + public void deleteById(Long id) { + airportService.deleteById(id); + } + + @Override + public void deleteAll() { + airportService.deleteAll(); + } + + @Override + public AirportDto update(Long id, NewAirportDtoRequest newAirportDtoRequest) { + gpsLocationValidator.validate(newAirportDtoRequest.getLocation().getLatitude(), + newAirportDtoRequest.getLocation().getLongitude()); + + if (!airportService.existsById(id)) { + throw new ResourceNotFoundException(); + } + Airport newEntity = airportMapper.toEntityFromNewRequest(newAirportDtoRequest); + Airport updatedEntity = airportService.update(id, newEntity); + return airportMapper.toDto(updatedEntity); + } + + @Override + public Optional<AirportDto> findByName(String name) { + Optional<Airport> foundEntity = airportService.findByName(name); + return foundEntity.map(airportMapper::toDto); + } + + @Override + public Optional<AirportDto> findByCode(String code) { + Optional<Airport> foundEntity = airportService.findByCode(code); + return foundEntity.map(airportMapper::toDto); + } + + @Override + public List<AirportDto> findByCity(Long cityId) { + City city = cityService.findById(cityId) + .orElseThrow(ResourceNotFoundException::new); + List<Airport> foundAirports = airportService.findByCity(city); + return foundAirports.stream() + .map(airportMapper::toDto) + .toList(); + } + + @Override + public AirportDto addArrivingFlightAssignment(Long airportId, Long flightId) { + Airport airport = airportService.findById(airportId) + .orElseThrow(ResourceNotFoundException::new); + Flight flight = flightService.findById(flightId) + .orElseThrow(ResourceNotFoundException::new); + + airport.addArrivingFlight(flight); + flight.setArrivingAirport(airport); + + Airport updatedAirport = airportService.update(airportId, airport); + flightService.update(flightId, flight); + + return airportMapper.toDto(updatedAirport); + } + + @Override + public AirportDto deleteArrivingFlightAssignment(Long airportId, Long flightId) { + Airport airport = airportService.findById(airportId) + .orElseThrow(ResourceNotFoundException::new); + Flight flight = flightService.findById(flightId) + .orElseThrow(ResourceNotFoundException::new); + + airport.removeArrivingFlight(flight); + flight.setArrivingAirport(null); + + Airport updatedAirport = airportService.update(airportId, airport); + flightService.update(flightId, flight); + + return airportMapper.toDto(updatedAirport); + } + + @Override + public AirportDto addDepartingFlightAssignment(Long airportId, Long flightId) { + Airport airport = airportService.findById(airportId) + .orElseThrow(ResourceNotFoundException::new); + Flight flight = flightService.findById(flightId) + .orElseThrow(ResourceNotFoundException::new); + + airport.addDepartingFlight(flight); + flight.setDepartingAirport(airport); + + Airport updatedAirport = airportService.update(airportId, airport); + flightService.update(flightId, flight); + + return airportMapper.toDto(updatedAirport); + } + + @Override + public AirportDto deleteDepartingFlightAssignment(Long airportId, Long flightId) { + Airport airport = airportService.findById(airportId) + .orElseThrow(ResourceNotFoundException::new); + Flight flight = flightService.findById(flightId) + .orElseThrow(ResourceNotFoundException::new); + + airport.removeDepartingFlight(flight); + flight.setDepartingAirport(null); + + Airport updatedAirport = airportService.update(airportId, airport); + flightService.update(flightId, flight); + + return airportMapper.toDto(updatedAirport); + } + + @Override + public AirportDto addCityAssignment(Long airportId, Long cityId) { + Airport airport = airportService.findById(airportId) + .orElseThrow(ResourceNotFoundException::new); + City city = cityService.findById(cityId) + .orElseThrow(ResourceNotFoundException::new); + + airport.setCity(city); + city.addAirport(airport); + + Airport updatedAirport = airportService.update(airportId, airport); + cityService.update(cityId, city); + + return airportMapper.toDto(updatedAirport); + } + + @Override + public AirportDto deleteCityAssignment(Long airportId, Long cityId) { + Airport airport = airportService.findById(airportId) + .orElseThrow(ResourceNotFoundException::new); + City city = cityService.findById(cityId) + .orElseThrow(ResourceNotFoundException::new); + + airport.setCity(null); + city.removeAirport(airport); + + Airport updatedAirport = airportService.update(airportId, airport); + cityService.update(cityId, city); + + return airportMapper.toDto(updatedAirport); + } +} diff --git a/core/src/main/java/cz/muni/fi/pa165/core/facade/city/CityFacade.java b/core/src/main/java/cz/muni/fi/pa165/core/facade/city/CityFacade.java new file mode 100644 index 0000000000000000000000000000000000000000..865417e40e024b3c8b863bb8b7f4d1e57c7bbde8 --- /dev/null +++ b/core/src/main/java/cz/muni/fi/pa165/core/facade/city/CityFacade.java @@ -0,0 +1,31 @@ +package cz.muni.fi.pa165.core.facade.city; + +import cz.muni.fi.pa165.core.model.CityDto; +import cz.muni.fi.pa165.core.model.NewCityDtoRequest; + +import java.util.List; +import java.util.Optional; + +/** + * @param <K> Key + */ +public interface CityFacade<K> { + + CityDto save(NewCityDtoRequest newCityDtoRequest); + + Optional<CityDto> findById(K id); + + List<CityDto> findAll(); + + void deleteById(K id); + + void deleteAll(); + + CityDto update(K id, NewCityDtoRequest newCityDtoRequest); + + Optional<CityDto> findByName(String name); + + CityDto addCountryAssignment(K cityId, K countryId); + + void deleteCountryAssignment(K cityId, K countryId); +} diff --git a/core/src/main/java/cz/muni/fi/pa165/core/facade/city/CityFacadeImpl.java b/core/src/main/java/cz/muni/fi/pa165/core/facade/city/CityFacadeImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..353de7d11c8d4e5d7d384c3dc888d8a622bc9790 --- /dev/null +++ b/core/src/main/java/cz/muni/fi/pa165/core/facade/city/CityFacadeImpl.java @@ -0,0 +1,112 @@ +package cz.muni.fi.pa165.core.facade.city; + +import cz.muni.fi.pa165.core.data.domain.City; +import cz.muni.fi.pa165.core.data.domain.Country; +import cz.muni.fi.pa165.core.exceptions.ResourceNotFoundException; +import cz.muni.fi.pa165.core.mapper.CityMapper; +import cz.muni.fi.pa165.core.model.CityDto; +import cz.muni.fi.pa165.core.model.NewCityDtoRequest; +import cz.muni.fi.pa165.core.service.city.CityService; +import cz.muni.fi.pa165.core.service.country.CountryService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.List; +import java.util.Optional; + +@Service +public class CityFacadeImpl implements CityFacade<Long> { + + private final CityService cityService; + private final CountryService countryService; + private final CityMapper cityMapper; + + @Autowired + public CityFacadeImpl(CityService cityService, CountryService countryService, CityMapper cityMapper) { + this.cityService = cityService; + this.countryService = countryService; + this.cityMapper = cityMapper; + } + + @Override + public CityDto save(NewCityDtoRequest newCityDtoRequest) { + City entityToSave = cityMapper.toEntityFromNewRequest(newCityDtoRequest); + City savedEntity = cityService.save(entityToSave); + return cityMapper.toDto(savedEntity); + } + + @Override + public Optional<CityDto> findById(Long id) { + City foundEntity = cityService.findById(id) + .orElseThrow(ResourceNotFoundException::new); + return Optional.of(cityMapper.toDto(foundEntity)); + } + + @Override + public List<CityDto> findAll() { + List<City> foundEntities = cityService.findAll(); + return foundEntities.stream() + .map(cityMapper::toDto) + .toList(); + } + + @Override + public void deleteById(Long id) { + if (!cityService.existsById(id)) { + throw new ResourceNotFoundException(); + } + cityService.deleteById(id); + } + + @Override + public void deleteAll() { + cityService.deleteAll(); + } + + @Override + public CityDto update(Long id, NewCityDtoRequest newCityDtoRequest) { + if (!cityService.existsById(id)) { + throw new ResourceNotFoundException(); + } + + City newEntity = cityMapper.toEntityFromNewRequest(newCityDtoRequest); + City updatedEntity = cityService.update(id, newEntity); + return cityMapper.toDto(updatedEntity); + } + + @Override + public Optional<CityDto> findByName(String name) { + Optional<City> foundEntity = cityService.findByName(name); + return foundEntity.map(cityMapper::toDto); + } + + @Override + public CityDto addCountryAssignment(Long cityId, Long countryId) { + City city = cityService.findById(cityId) + .orElseThrow(ResourceNotFoundException::new); + Country country = countryService.findById(countryId) + .orElseThrow(ResourceNotFoundException::new); + + country.addCity(city); + city.setCountry(country); + + countryService.update(countryId, country); + City updatedCity = cityService.update(cityId, city); + + return cityMapper.toDto(updatedCity); + } + + @Override + public void deleteCountryAssignment(Long cityId, Long countryId) { + City city = cityService.findById(cityId) + .orElseThrow(ResourceNotFoundException::new); + Country country = countryService.findById(countryId) + .orElseThrow(ResourceNotFoundException::new); + + country.removeCity(city); + city.setCountry(null); + + countryService.update(countryId, country); + cityService.update(cityId, city); + } +} diff --git a/core/src/main/java/cz/muni/fi/pa165/core/facade/country/CountryFacade.java b/core/src/main/java/cz/muni/fi/pa165/core/facade/country/CountryFacade.java new file mode 100644 index 0000000000000000000000000000000000000000..801cdbac840dee4bf5acbe266f56d4b32ee2cc25 --- /dev/null +++ b/core/src/main/java/cz/muni/fi/pa165/core/facade/country/CountryFacade.java @@ -0,0 +1,27 @@ +package cz.muni.fi.pa165.core.facade.country; + +import cz.muni.fi.pa165.core.model.CountryDto; +import cz.muni.fi.pa165.core.model.NewCountryDtoRequest; + +import java.util.List; +import java.util.Optional; + +/** + * @param <K> Key + */ +public interface CountryFacade<K> { + + CountryDto save(NewCountryDtoRequest newCountryDtoRequest); + + Optional<CountryDto> findById(K id); + + List<CountryDto> findAll(); + + void deleteById(K id); + + void deleteAll(); + + CountryDto update(K id, NewCountryDtoRequest newCountryDtoRequest); + + Optional<CountryDto> findByName(String name); +} diff --git a/core/src/main/java/cz/muni/fi/pa165/core/facade/country/CountryFacadeImpl.java b/core/src/main/java/cz/muni/fi/pa165/core/facade/country/CountryFacadeImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..b5b957635b149e39537a0b042c69711105744c0e --- /dev/null +++ b/core/src/main/java/cz/muni/fi/pa165/core/facade/country/CountryFacadeImpl.java @@ -0,0 +1,78 @@ +package cz.muni.fi.pa165.core.facade.country; + +import cz.muni.fi.pa165.core.data.domain.Country; +import cz.muni.fi.pa165.core.exceptions.ResourceNotFoundException; +import cz.muni.fi.pa165.core.mapper.CountryMapper; +import cz.muni.fi.pa165.core.model.CountryDto; +import cz.muni.fi.pa165.core.model.NewCountryDtoRequest; +import cz.muni.fi.pa165.core.service.country.CountryService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.List; +import java.util.Optional; + +@Service +public class CountryFacadeImpl implements CountryFacade<Long> { + + private final CountryService countryService; + private final CountryMapper countryMapper; + + @Autowired + public CountryFacadeImpl(CountryService countryService, CountryMapper countryMapper) { + this.countryService = countryService; + this.countryMapper = countryMapper; + } + + @Override + public CountryDto save(NewCountryDtoRequest newCountryDtoRequest) { + Country entityToSave = countryMapper.toEntityFromNewRequest(newCountryDtoRequest); + Country savedEntity = countryService.save(entityToSave); + return countryMapper.toDto(savedEntity); + } + + @Override + public Optional<CountryDto> findById(Long id) { + Country foundEntity = countryService.findById(id) + .orElseThrow(ResourceNotFoundException::new); + return Optional.of(countryMapper.toDto(foundEntity)); + } + + @Override + public List<CountryDto> findAll() { + List<Country> foundEntities = countryService.findAll(); + return foundEntities.stream() + .map(countryMapper::toDto) + .toList(); + } + + @Override + public void deleteById(Long id) { + if (!countryService.existsById(id)) { + throw new ResourceNotFoundException(); + } + countryService.deleteById(id); + } + + @Override + public void deleteAll() { + countryService.deleteAll(); + } + + @Override + public CountryDto update(Long id, NewCountryDtoRequest newCountryDtoRequest) { + if (!countryService.existsById(id)) { + throw new ResourceNotFoundException(); + } + + Country newEntity = countryMapper.toEntityFromNewRequest(newCountryDtoRequest); + Country updatedEntity = countryService.update(id, newEntity); + return countryMapper.toDto(updatedEntity); + } + + @Override + public Optional<CountryDto> findByName(String name) { + Optional<Country> foundEntity = countryService.findByName(name); + return foundEntity.map(countryMapper::toDto); + } +} diff --git a/core/src/main/java/cz/muni/fi/pa165/core/facade/flight/FlightFacade.java b/core/src/main/java/cz/muni/fi/pa165/core/facade/flight/FlightFacade.java new file mode 100644 index 0000000000000000000000000000000000000000..acee09cfad187e4bf66f2f2641fdc5d8ed031cce --- /dev/null +++ b/core/src/main/java/cz/muni/fi/pa165/core/facade/flight/FlightFacade.java @@ -0,0 +1,24 @@ +package cz.muni.fi.pa165.core.facade.flight; + +import cz.muni.fi.pa165.core.model.FlightDto; +import cz.muni.fi.pa165.core.model.NewFlightDtoRequest; + +import java.util.List; +import java.util.Optional; + +/** + * @param <K> Key + * @author martinslovik + */ +public interface FlightFacade<K> { + + FlightDto save(NewFlightDtoRequest newFlightDtoRequest); + + Optional<FlightDto> findById(K id); + + List<FlightDto> findAll(); + + void deleteById(K id); + + FlightDto update(Long id, NewFlightDtoRequest newFlightDtoRequest); +} diff --git a/core/src/main/java/cz/muni/fi/pa165/core/facade/flight/FlightFacadeImpl.java b/core/src/main/java/cz/muni/fi/pa165/core/facade/flight/FlightFacadeImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..75fc0190d553cc19af4517290aabaa5333f83947 --- /dev/null +++ b/core/src/main/java/cz/muni/fi/pa165/core/facade/flight/FlightFacadeImpl.java @@ -0,0 +1,79 @@ +package cz.muni.fi.pa165.core.facade.flight; + +import cz.muni.fi.pa165.core.exceptions.ResourceNotFoundException; +import cz.muni.fi.pa165.core.mapper.FlightMapper; +import cz.muni.fi.pa165.core.model.FlightDto; +import cz.muni.fi.pa165.core.model.NewFlightDtoRequest; +import cz.muni.fi.pa165.core.service.airplane.AirplaneService; +import cz.muni.fi.pa165.core.service.flight.FlightService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.List; +import java.util.Optional; + +@Service +public class FlightFacadeImpl implements FlightFacade<Long> { + + private final FlightService flightService; + private final AirplaneService airplaneService; + private final FlightMapper flightMapper; + + @Autowired + public FlightFacadeImpl(FlightService flightService, AirplaneService airplaneService, FlightMapper flightMapper) { + this.flightService = flightService; + this.airplaneService = airplaneService; + this.flightMapper = flightMapper; + } + + @Override + public FlightDto save(NewFlightDtoRequest newFlightDtoRequest) { + var airplane = airplaneService.findById(newFlightDtoRequest.getAirplaneId()) + .orElseThrow(ResourceNotFoundException::new); + var entityToSave = flightMapper.toEntityFromNewRequest(newFlightDtoRequest); + entityToSave.setAirplane(airplane); + var savedEntity = flightService.save(entityToSave); + return flightMapper.toDto(savedEntity); + } + + @Override + public Optional<FlightDto> findById(Long id) { + var foundFlightEntity = flightService.findById(id) + .orElseThrow(ResourceNotFoundException::new); + + var flightDto = flightMapper.toDto(foundFlightEntity); + + return Optional.of(flightDto); + } + + @Override + public List<FlightDto> findAll() { + var foundFlights = flightService.findAll(); + + return foundFlights + .stream() + .map(flightMapper::toDto) + .toList(); + } + + @Override + public void deleteById(Long id) { + if (!flightService.existsById(id)) { + throw new ResourceNotFoundException(); + + } + flightService.deleteById(id); + } + + @Override + public FlightDto update(Long id, NewFlightDtoRequest newFlightDtoRequest) { + if (!flightService.existsById(id)) { + throw new ResourceNotFoundException(); + + } + var newFlightEntity = flightMapper.toEntityFromNewRequest(newFlightDtoRequest); + var updatedFlightEntity = flightService.update(id, newFlightEntity); + + return flightMapper.toDto(updatedFlightEntity); + } +} diff --git a/core/src/main/java/cz/muni/fi/pa165/core/facade/steward/StewardFacade.java b/core/src/main/java/cz/muni/fi/pa165/core/facade/steward/StewardFacade.java new file mode 100644 index 0000000000000000000000000000000000000000..8e2ab914ef529b3c0138b964d8e662c813adae8b --- /dev/null +++ b/core/src/main/java/cz/muni/fi/pa165/core/facade/steward/StewardFacade.java @@ -0,0 +1,28 @@ +package cz.muni.fi.pa165.core.facade.steward; + +import cz.muni.fi.pa165.core.model.NewStewardDtoRequest; +import cz.muni.fi.pa165.core.model.StewardDto; + +import java.util.List; +import java.util.Optional; + +/** + * @param <K> Key + * @author martinslovik + */ +public interface StewardFacade<K> { + + StewardDto save(NewStewardDtoRequest newStewardDtoRequest); + + Optional<StewardDto> findById(K id); + + List<StewardDto> findAll(); + + void deleteById(K id); + + StewardDto update(Long id, NewStewardDtoRequest newStewardDtoRequest); + + StewardDto assignStewardFlight(Long stewardId, Long flightId); + + void deleteStewardFlightsAssignment(Long stewardId, Long flightId); +} diff --git a/core/src/main/java/cz/muni/fi/pa165/core/facade/steward/StewardFacadeImpl.java b/core/src/main/java/cz/muni/fi/pa165/core/facade/steward/StewardFacadeImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..5cceefd2efa770ef4d17a325d00e35b4d33c9d1e --- /dev/null +++ b/core/src/main/java/cz/muni/fi/pa165/core/facade/steward/StewardFacadeImpl.java @@ -0,0 +1,104 @@ +package cz.muni.fi.pa165.core.facade.steward; + +import cz.muni.fi.pa165.core.data.domain.FlightSteward; +import cz.muni.fi.pa165.core.exceptions.ResourceNotFoundException; +import cz.muni.fi.pa165.core.mapper.StewardMapper; +import cz.muni.fi.pa165.core.model.NewStewardDtoRequest; +import cz.muni.fi.pa165.core.model.StewardDto; +import cz.muni.fi.pa165.core.service.flight.FlightService; +import cz.muni.fi.pa165.core.service.steward.StewardService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.List; +import java.util.Optional; + +@Service +public class StewardFacadeImpl implements StewardFacade<Long> { + + private final StewardService stewardService; + private final FlightService flightService; + private final StewardMapper stewardMapper; + + + @Autowired + public StewardFacadeImpl(StewardService stewardService, FlightService flightService, StewardMapper stewardMapper) { + this.stewardService = stewardService; + this.flightService = flightService; + this.stewardMapper = stewardMapper; + } + + @Override + public StewardDto save(NewStewardDtoRequest newStewardDtoRequest) { + var entityToSave = stewardMapper.toEntityFromNewRequest(newStewardDtoRequest); + var savedEntity = stewardService.save(entityToSave); + return stewardMapper.toDto(savedEntity); + } + + @Override + public Optional<StewardDto> findById(Long id) { + var foundStewardEntity = stewardService.findById(id) + .orElseThrow(ResourceNotFoundException::new); + + var stewardDto = stewardMapper.toDto(foundStewardEntity); + + return Optional.of(stewardDto); + } + + @Override + public List<StewardDto> findAll() { + var foundStewards = stewardService.findAll(); + + return foundStewards + .stream() + .map(stewardMapper::toDto) + .toList(); + } + + @Override + public void deleteById(Long id) { + if (!stewardService.existsById(id)) { + throw new ResourceNotFoundException(); + } + stewardService.deleteById(id); + } + + @Override + public StewardDto update(Long id, NewStewardDtoRequest newStewardDtoRequest) { + if (!stewardService.existsById(id)) { + throw new ResourceNotFoundException(); + } + var newStewardEntity = stewardMapper.toEntityFromNewRequest(newStewardDtoRequest); + var updatedStewardEntity = stewardService.update(id, newStewardEntity); + + return stewardMapper.toDto(updatedStewardEntity); + } + + @Override + public StewardDto assignStewardFlight(Long stewardId, Long flightId) { + var stewardEntity = stewardService.findByIdWithFlights(stewardId) + .orElse(stewardService.findById(stewardId) + .orElseThrow(ResourceNotFoundException::new)); + + var flightEntity = flightService.findById(flightId) + .orElseThrow(ResourceNotFoundException::new); + + var flightSteward = new FlightSteward(); + flightSteward.setSteward(stewardEntity); + flightSteward.setFlight(flightEntity); + stewardService.saveFlightStewards(flightSteward); + + return stewardMapper.toDto(stewardEntity); + } + + @Override + public void deleteStewardFlightsAssignment(Long stewardId, Long flightId) { + var stewardEntity = stewardService.findById(stewardId) + .orElseThrow(ResourceNotFoundException::new); + + var flightEntity = flightService.findById(flightId) + .orElseThrow(ResourceNotFoundException::new); + + stewardService.deleteFlightStewards(stewardEntity, flightEntity); + } +} diff --git a/core/src/main/java/cz/muni/fi/pa165/core/mapper/AirplaneMapper.java b/core/src/main/java/cz/muni/fi/pa165/core/mapper/AirplaneMapper.java new file mode 100644 index 0000000000000000000000000000000000000000..dd9336c7a122fc555aea3c7156e6f13b16561889 --- /dev/null +++ b/core/src/main/java/cz/muni/fi/pa165/core/mapper/AirplaneMapper.java @@ -0,0 +1,27 @@ +package cz.muni.fi.pa165.core.mapper; + +import cz.muni.fi.pa165.core.data.domain.Airplane; +import cz.muni.fi.pa165.core.data.domain.AirplaneType; +import cz.muni.fi.pa165.core.model.AirplaneDto; +import cz.muni.fi.pa165.core.model.NewAirplaneDtoRequest; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +@Component +@Mapper(componentModel = "spring") +public abstract class AirplaneMapper { + + @Autowired + private AirplaneTypeMapper airplaneTypeMapper; + + public abstract Airplane toEntityFromNewRequest(NewAirplaneDtoRequest newAirplaneDtoRequest); + + @Mapping(target = "typeId", source = "type") + public abstract AirplaneDto toDto(Airplane airplane); + + protected Long getAirplanesTypeId(AirplaneType airplaneType) { + return airplaneTypeMapper.getAirplanesTypeId(airplaneType); + } +} diff --git a/core/src/main/java/cz/muni/fi/pa165/core/mapper/AirplaneTypeMapper.java b/core/src/main/java/cz/muni/fi/pa165/core/mapper/AirplaneTypeMapper.java new file mode 100644 index 0000000000000000000000000000000000000000..47f858281c3ba1d5a7e4d4fbbac4ebd79a9c265b --- /dev/null +++ b/core/src/main/java/cz/muni/fi/pa165/core/mapper/AirplaneTypeMapper.java @@ -0,0 +1,30 @@ +package cz.muni.fi.pa165.core.mapper; + +import cz.muni.fi.pa165.core.data.domain.Airplane; +import cz.muni.fi.pa165.core.data.domain.AirplaneType; +import cz.muni.fi.pa165.core.model.AirplaneTypeDto; +import cz.muni.fi.pa165.core.model.NewAirplaneTypeDtoRequest; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; + +import java.util.List; + +@Mapper(componentModel = "spring") +public interface AirplaneTypeMapper { + + AirplaneType toEntityFromNewRequest(NewAirplaneTypeDtoRequest newAirplaneTypeDtoRequest); + + @Mapping(target = "airplanesIds", source = "airplanes") + AirplaneTypeDto toDto(AirplaneType airplaneType); + + default Long getAirplanesTypeId(AirplaneType airplaneType) { + return airplaneType == null ? 0L : airplaneType.getId(); + } + + default List<Long> mapAirplaneTypesToIds(List<Airplane> airplanes) { + return airplanes + .stream() + .map(Airplane::getId) + .toList(); + } +} diff --git a/core/src/main/java/cz/muni/fi/pa165/core/mapper/AirportMapper.java b/core/src/main/java/cz/muni/fi/pa165/core/mapper/AirportMapper.java new file mode 100644 index 0000000000000000000000000000000000000000..a8764aa894456a704113fa002eee63944ef8be7e --- /dev/null +++ b/core/src/main/java/cz/muni/fi/pa165/core/mapper/AirportMapper.java @@ -0,0 +1,39 @@ +package cz.muni.fi.pa165.core.mapper; + +import cz.muni.fi.pa165.core.data.domain.Airport; +import cz.muni.fi.pa165.core.data.domain.City; +import cz.muni.fi.pa165.core.data.domain.Flight; +import cz.muni.fi.pa165.core.model.AirportDto; +import cz.muni.fi.pa165.core.model.NewAirportDtoRequest; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.springframework.stereotype.Component; + +import java.util.List; + +@Component +@Mapper(componentModel = "spring") +public abstract class AirportMapper { + + @Mapping(target = "latitude", source = "location.latitude") + @Mapping(target = "longitude", source = "location.longitude") + public abstract Airport toEntityFromNewRequest(NewAirportDtoRequest newAirportDtoRequest); + + @Mapping(target = "cityId", source = "city") + @Mapping(target = "arrivingFlightsIds", source = "arrivingFlights") + @Mapping(target = "departingFlightsIds", source = "departingFlights") + @Mapping(target = "location.latitude", source = "latitude") + @Mapping(target = "location.longitude", source = "longitude") + public abstract AirportDto toDto(Airport airport); + + protected Long getCityId(City city) { + return city == null ? 0L : city.getId(); + } + + protected List<Long> mapFlightsToIds(List<Flight> flights) { + return flights + .stream() + .map(Flight::getId) + .toList(); + } +} diff --git a/core/src/main/java/cz/muni/fi/pa165/core/mapper/CityMapper.java b/core/src/main/java/cz/muni/fi/pa165/core/mapper/CityMapper.java new file mode 100644 index 0000000000000000000000000000000000000000..ffbcf492c017212163e474a55505d172385be102 --- /dev/null +++ b/core/src/main/java/cz/muni/fi/pa165/core/mapper/CityMapper.java @@ -0,0 +1,32 @@ +package cz.muni.fi.pa165.core.mapper; + +import cz.muni.fi.pa165.core.data.domain.Airport; +import cz.muni.fi.pa165.core.data.domain.City; +import cz.muni.fi.pa165.core.data.domain.Country; +import cz.muni.fi.pa165.core.model.CityDto; +import cz.muni.fi.pa165.core.model.NewCityDtoRequest; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; + +import java.util.List; + +@Mapper(componentModel = "spring") +public interface CityMapper { + + City toEntityFromNewRequest(NewCityDtoRequest newCityDtoRequest); + + @Mapping(target = "countryId", source = "country") + @Mapping(target = "airportsIds", source = "airports") + CityDto toDto(City city); + + default Long getCountryId(Country country) { + return country == null ? 0L : country.getId(); + } + + default List<Long> mapAirportsToIds(List<Airport> airports) { + return airports + .stream() + .map(Airport::getId) + .toList(); + } +} diff --git a/core/src/main/java/cz/muni/fi/pa165/core/mapper/CountryMapper.java b/core/src/main/java/cz/muni/fi/pa165/core/mapper/CountryMapper.java new file mode 100644 index 0000000000000000000000000000000000000000..e270fd36e79538609c9bf90fab9470b8c2824ae0 --- /dev/null +++ b/core/src/main/java/cz/muni/fi/pa165/core/mapper/CountryMapper.java @@ -0,0 +1,26 @@ +package cz.muni.fi.pa165.core.mapper; + +import cz.muni.fi.pa165.core.data.domain.City; +import cz.muni.fi.pa165.core.data.domain.Country; +import cz.muni.fi.pa165.core.model.CountryDto; +import cz.muni.fi.pa165.core.model.NewCountryDtoRequest; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; + +import java.util.List; + +@Mapper(componentModel = "spring") +public interface CountryMapper { + + Country toEntityFromNewRequest(NewCountryDtoRequest newCountryDtoRequest); + + @Mapping(target = "citiesIds", source = "cities") + CountryDto toDto(Country country); + + default List<Long> mapCitiesToIds(List<City> cities) { + return cities + .stream() + .map(City::getId) + .toList(); + } +} diff --git a/core/src/main/java/cz/muni/fi/pa165/core/mapper/FlightMapper.java b/core/src/main/java/cz/muni/fi/pa165/core/mapper/FlightMapper.java new file mode 100644 index 0000000000000000000000000000000000000000..f5e82c7e5b6b36cd157d9aa4f525c183a7265f72 --- /dev/null +++ b/core/src/main/java/cz/muni/fi/pa165/core/mapper/FlightMapper.java @@ -0,0 +1,44 @@ +package cz.muni.fi.pa165.core.mapper; + +import cz.muni.fi.pa165.core.data.domain.Airplane; +import cz.muni.fi.pa165.core.data.domain.Airport; +import cz.muni.fi.pa165.core.data.domain.Flight; +import cz.muni.fi.pa165.core.data.domain.FlightSteward; +import cz.muni.fi.pa165.core.model.FlightDto; +import cz.muni.fi.pa165.core.model.NewFlightDtoRequest; +import org.mapstruct.*; + +import java.util.ArrayList; +import java.util.List; + +@Mapper(componentModel = "spring") +public interface FlightMapper { + + Flight toEntityFromNewRequest(NewFlightDtoRequest newFlightDtoRequest); + + default List<Long> toStewardIdList(List<FlightSteward> flightStewards) { + if (flightStewards != null) { + return flightStewards.stream() + .map(fs -> fs.getSteward().getId()) + .toList(); + } + return new ArrayList<>(); + } + + default Long toAirplaneId(Airplane airplane) { + return airplane == null ? 0L : airplane.getId(); + } + + default Long toAirportId(Airport airport) { + return airport == null ? 0L : airport.getId(); + } + + @Mapping(target = "assignedStewardIds", expression = "java(toStewardIdList(flight.getFlightStewards()))") + @Mapping(target = "airplaneId", expression = "java(toAirplaneId(flight.getAirplane()))") + @Mapping(target = "departureAirportId", expression = "java(toAirportId(flight.getDepartingAirport()))") + @Mapping(target = "arrivalAirportId", expression = "java(toAirportId(flight.getArrivingAirport()))") + FlightDto toDto(Flight flight); + + @BeanMapping(nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE) + Flight partialUpdate(FlightDto flightDto, @MappingTarget Flight flight); +} diff --git a/core/src/main/java/cz/muni/fi/pa165/core/mapper/StewardMapper.java b/core/src/main/java/cz/muni/fi/pa165/core/mapper/StewardMapper.java new file mode 100644 index 0000000000000000000000000000000000000000..42e1ca3c98fba5a1c3f7455af0a031b275fe9a0d --- /dev/null +++ b/core/src/main/java/cz/muni/fi/pa165/core/mapper/StewardMapper.java @@ -0,0 +1,29 @@ +package cz.muni.fi.pa165.core.mapper; + +import cz.muni.fi.pa165.core.data.domain.FlightSteward; +import cz.muni.fi.pa165.core.data.domain.Steward; +import cz.muni.fi.pa165.core.model.NewStewardDtoRequest; +import cz.muni.fi.pa165.core.model.StewardDto; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; + +import java.util.ArrayList; +import java.util.List; + +@Mapper(componentModel = "spring") +public interface StewardMapper { + + Steward toEntityFromNewRequest(NewStewardDtoRequest newStewardDtoRequest); + + default List<Long> toFlightIdList(List<FlightSteward> flightStewards) { + if (flightStewards != null) { + return flightStewards.stream() + .map(fs -> fs.getFlight().getId()) + .toList(); + } + return new ArrayList<>(); + } + + @Mapping(target = "assignedFlightIds", expression = "java(toFlightIdList(steward.getFlightStewards()))") + StewardDto toDto(Steward steward); +} diff --git a/core/src/main/java/cz/muni/fi/pa165/core/rest/AirplaneController.java b/core/src/main/java/cz/muni/fi/pa165/core/rest/AirplaneController.java index 143e24e85c08429f18855594d6c14d3004f69c7c..f7ef9a5ec76d5fb2095033d6c4b357f7024ef1b2 100644 --- a/core/src/main/java/cz/muni/fi/pa165/core/rest/AirplaneController.java +++ b/core/src/main/java/cz/muni/fi/pa165/core/rest/AirplaneController.java @@ -2,8 +2,10 @@ package cz.muni.fi.pa165.core.rest; import cz.muni.fi.pa165.core.api.AirplaneApi; import cz.muni.fi.pa165.core.api.AirplaneApiDelegate; +import cz.muni.fi.pa165.core.facade.airplane.AirplaneFacade; import cz.muni.fi.pa165.core.model.AirplaneDto; import cz.muni.fi.pa165.core.model.NewAirplaneDtoRequest; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.RestController; @@ -12,20 +14,14 @@ import java.util.List; @RestController public class AirplaneController implements AirplaneApiDelegate { - /** - * POST /api/airplanes/{airplaneId}/airplaneTypes/{airplaneTypeId} : Assign airplane type to a airplane. - * - * @param airplaneId (required) - * @param airplaneTypeId (required) - * @return OK (status code 201) - * or Input data not correct (status code 400) - * @see AirplaneApi#assignAirplaneType - */ - @Override - public ResponseEntity<AirplaneDto> assignAirplaneType(Long airplaneId, Long airplaneTypeId) { - return AirplaneApiDelegate.super.assignAirplaneType(airplaneId, airplaneTypeId); + private final AirplaneFacade<Long> airplaneFacade; + + @Autowired + public AirplaneController(AirplaneFacade<Long> airplaneFacade) { + this.airplaneFacade = airplaneFacade; } + /** * POST /api/airplanes : Create a new airplane. * Creates a new airplane and returns it as a response. @@ -37,7 +33,7 @@ public class AirplaneController implements AirplaneApiDelegate { */ @Override public ResponseEntity<AirplaneDto> createAirplane(NewAirplaneDtoRequest newAirplaneDtoRequest) { - return AirplaneApiDelegate.super.createAirplane(newAirplaneDtoRequest); + return ResponseEntity.ok(airplaneFacade.save(newAirplaneDtoRequest)); } /** @@ -50,22 +46,10 @@ public class AirplaneController implements AirplaneApiDelegate { */ @Override public ResponseEntity<Void> deleteAirplane(Long id) { - return AirplaneApiDelegate.super.deleteAirplane(id); + airplaneFacade.deleteById(id); + return null; } - /** - * DELETE /api/airplanes/{airplaneId}/airplaneTypes/{airplaneTypeId} : Delete assignment of airplane type to an airplane. - * - * @param airplaneId (required) - * @param airplaneTypeId (required) - * @return Deleted (status code 204) - * or Not Found (status code 404) - * @see AirplaneApi#deleteAirplaneTypeAssignment - */ - @Override - public ResponseEntity<Void> deleteAirplaneTypeAssignment(Long airplaneId, Long airplaneTypeId) { - return AirplaneApiDelegate.super.deleteAirplaneTypeAssignment(airplaneId, airplaneTypeId); - } /** * GET /api/airplanes/{id} : Get airplane by id. @@ -77,7 +61,7 @@ public class AirplaneController implements AirplaneApiDelegate { */ @Override public ResponseEntity<AirplaneDto> getAirplaneById(Long id) { - return AirplaneApiDelegate.super.getAirplaneById(id); + return ResponseEntity.of(airplaneFacade.findById(id)); } /** @@ -89,7 +73,7 @@ public class AirplaneController implements AirplaneApiDelegate { */ @Override public ResponseEntity<List<AirplaneDto>> getAllAirplanes() { - return AirplaneApiDelegate.super.getAllAirplanes(); + return ResponseEntity.ok(airplaneFacade.findAll()); } /** @@ -97,27 +81,14 @@ public class AirplaneController implements AirplaneApiDelegate { * Updates a airplane by id and returns it as a response. * * @param id (required) - * @param airplaneDto (required) + * @param newAirplaneDtoRequest (required) * @return OK (status code 200) * or Input data not correct (status code 400) - * @see AirplaneApi#updateAirplane - */ - @Override - public ResponseEntity<AirplaneDto> updateAirplane(Long id, AirplaneDto airplaneDto) { - return AirplaneApiDelegate.super.updateAirplane(id, airplaneDto); - } - - /** - * PUT /api/airplanes/{airplaneId}/airplaneTypes/{airplaneTypeId} : Update assignment of airplane type to an airplane. - * - * @param airplaneId (required) - * @param airplaneTypeId (required) - * @return OK (status code 200) * or Input data not correct (status code 400) - * @see AirplaneApi#updateAirplaneTypeAssignment + * @see AirplaneApi#updateAirplane */ @Override - public ResponseEntity<AirplaneDto> updateAirplaneTypeAssignment(Long airplaneId, Long airplaneTypeId) { - return AirplaneApiDelegate.super.updateAirplaneTypeAssignment(airplaneId, airplaneTypeId); + public ResponseEntity<AirplaneDto> updateAirplane(Long id, NewAirplaneDtoRequest newAirplaneDtoRequest) { + return ResponseEntity.ok(airplaneFacade.update(id, newAirplaneDtoRequest)); } } diff --git a/core/src/main/java/cz/muni/fi/pa165/core/rest/AirplaneTypeController.java b/core/src/main/java/cz/muni/fi/pa165/core/rest/AirplaneTypeController.java index 86419eaf19d9b606fc2a198fd9cac46bf29def19..43302987e78238ab402da20f4162ad42b205fb71 100644 --- a/core/src/main/java/cz/muni/fi/pa165/core/rest/AirplaneTypeController.java +++ b/core/src/main/java/cz/muni/fi/pa165/core/rest/AirplaneTypeController.java @@ -2,8 +2,10 @@ package cz.muni.fi.pa165.core.rest; import cz.muni.fi.pa165.core.api.AirplaneTypeApi; import cz.muni.fi.pa165.core.api.AirplaneTypeApiDelegate; +import cz.muni.fi.pa165.core.facade.airplanetype.AirplaneTypeFacade; import cz.muni.fi.pa165.core.model.AirplaneTypeDto; import cz.muni.fi.pa165.core.model.NewAirplaneTypeDtoRequest; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.RestController; @@ -12,6 +14,13 @@ import java.util.List; @RestController public class AirplaneTypeController implements AirplaneTypeApiDelegate { + private final AirplaneTypeFacade<Long> airplaneTypeFacade; + + @Autowired + public AirplaneTypeController(AirplaneTypeFacade<Long> airplaneTypeFacade) { + this.airplaneTypeFacade = airplaneTypeFacade; + } + /** * POST /api/airplaneTypes : Create a new airplane type. * Creates a new airplane type and returns it as a response. @@ -23,7 +32,7 @@ public class AirplaneTypeController implements AirplaneTypeApiDelegate { */ @Override public ResponseEntity<AirplaneTypeDto> createAirplaneType(NewAirplaneTypeDtoRequest newAirplaneTypeDtoRequest) { - return AirplaneTypeApiDelegate.super.createAirplaneType(newAirplaneTypeDtoRequest); + return ResponseEntity.ok(airplaneTypeFacade.save(newAirplaneTypeDtoRequest)); } /** @@ -36,7 +45,8 @@ public class AirplaneTypeController implements AirplaneTypeApiDelegate { */ @Override public ResponseEntity<Void> deleteAirplaneType(Long id) { - return AirplaneTypeApiDelegate.super.deleteAirplaneType(id); + airplaneTypeFacade.deleteById(id); + return null; } /** @@ -49,7 +59,7 @@ public class AirplaneTypeController implements AirplaneTypeApiDelegate { */ @Override public ResponseEntity<AirplaneTypeDto> getAirplaneTypeById(Long id) { - return AirplaneTypeApiDelegate.super.getAirplaneTypeById(id); + return ResponseEntity.of(airplaneTypeFacade.findById(id)); } /** @@ -61,7 +71,7 @@ public class AirplaneTypeController implements AirplaneTypeApiDelegate { */ @Override public ResponseEntity<List<AirplaneTypeDto>> getAllAirplaneTypes() { - return AirplaneTypeApiDelegate.super.getAllAirplaneTypes(); + return ResponseEntity.ok(airplaneTypeFacade.findAll()); } /** @@ -69,13 +79,13 @@ public class AirplaneTypeController implements AirplaneTypeApiDelegate { * Updates a airplane type by id and returns it as a response. * * @param id (required) - * @param airplaneTypeDto (required) + * @param newAirplaneTypeDtoRequest (required) * @return OK (status code 200) * or Input data not correct (status code 400) * @see AirplaneTypeApi#updateAirplaneType */ @Override - public ResponseEntity<AirplaneTypeDto> updateAirplaneType(Long id, AirplaneTypeDto airplaneTypeDto) { - return AirplaneTypeApiDelegate.super.updateAirplaneType(id, airplaneTypeDto); + public ResponseEntity<AirplaneTypeDto> updateAirplaneType(Long id, NewAirplaneTypeDtoRequest newAirplaneTypeDtoRequest) { + return ResponseEntity.ok(airplaneTypeFacade.update(id, newAirplaneTypeDtoRequest)); } } diff --git a/core/src/main/java/cz/muni/fi/pa165/core/rest/AirportController.java b/core/src/main/java/cz/muni/fi/pa165/core/rest/AirportController.java index 46747877632af17c6da12c45eac404c1427c216b..93242ce6df90282fd3ccbc2d10537e2691d27736 100644 --- a/core/src/main/java/cz/muni/fi/pa165/core/rest/AirportController.java +++ b/core/src/main/java/cz/muni/fi/pa165/core/rest/AirportController.java @@ -2,8 +2,10 @@ package cz.muni.fi.pa165.core.rest; import cz.muni.fi.pa165.core.api.AirportApi; import cz.muni.fi.pa165.core.api.AirportApiDelegate; +import cz.muni.fi.pa165.core.facade.airport.AirportFacade; import cz.muni.fi.pa165.core.model.AirportDto; import cz.muni.fi.pa165.core.model.NewAirportDtoRequest; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.RestController; @@ -12,6 +14,13 @@ import java.util.List; @RestController public class AirportController implements AirportApiDelegate { + private final AirportFacade<Long> airportFacade; + + @Autowired + public AirportController(AirportFacade<Long> airportFacade) { + this.airportFacade = airportFacade; + } + /** * POST /api/airports/{airportId}/arrivingFlights/{arrivingFlightId} : Assign arriving flight to an airport. * @@ -23,7 +32,7 @@ public class AirportController implements AirportApiDelegate { */ @Override public ResponseEntity<AirportDto> assignArrivingFlight(Long airportId, Long arrivingFlightId) { - return AirportApiDelegate.super.assignArrivingFlight(airportId, arrivingFlightId); + return ResponseEntity.ok(airportFacade.addArrivingFlightAssignment(airportId, arrivingFlightId)); } /** @@ -37,7 +46,21 @@ public class AirportController implements AirportApiDelegate { */ @Override public ResponseEntity<AirportDto> assignDepartingFlight(Long airportId, Long departingFlightId) { - return AirportApiDelegate.super.assignDepartingFlight(airportId, departingFlightId); + return ResponseEntity.ok(airportFacade.addDepartingFlightAssignment(airportId, departingFlightId)); + } + + /** + * POST /api/airports/{airportId}/city/{cityId} : Assign the city to an airport. + * + * @param airportId (required) + * @param cityId (required) + * @return OK (status code 201) + * or Input data not correct (status code 400) + * @see AirportApi#assignCity + */ + @Override + public ResponseEntity<AirportDto> assignCity(Long airportId, Long cityId) { + return ResponseEntity.ok(airportFacade.addCityAssignment(airportId, cityId)); } /** @@ -51,7 +74,7 @@ public class AirportController implements AirportApiDelegate { */ @Override public ResponseEntity<AirportDto> createAirport(NewAirportDtoRequest newAirportDtoRequest) { - return AirportApiDelegate.super.createAirport(newAirportDtoRequest); + return ResponseEntity.ok(airportFacade.save(newAirportDtoRequest)); } /** @@ -64,7 +87,8 @@ public class AirportController implements AirportApiDelegate { */ @Override public ResponseEntity<Void> deleteAirport(Long id) { - return AirportApiDelegate.super.deleteAirport(id); + airportFacade.deleteById(id); + return null; } /** @@ -78,7 +102,8 @@ public class AirportController implements AirportApiDelegate { */ @Override public ResponseEntity<Void> deleteArrivingFlightAssignment(Long airportId, Long arrivingFlightId) { - return AirportApiDelegate.super.deleteArrivingFlightAssignment(airportId, arrivingFlightId); + airportFacade.deleteArrivingFlightAssignment(airportId, arrivingFlightId); + return null; } /** @@ -92,7 +117,23 @@ public class AirportController implements AirportApiDelegate { */ @Override public ResponseEntity<Void> deleteDepartingFlightAssignment(Long airportId, Long departingFlightId) { - return AirportApiDelegate.super.deleteDepartingFlightAssignment(airportId, departingFlightId); + airportFacade.deleteDepartingFlightAssignment(airportId, departingFlightId); + return null; + } + + /** + * DELETE /api/airports/{airportId}/city/{cityId} : Delete assignment of the city to an airport. + * + * @param airportId (required) + * @param cityId (required) + * @return Deleted (status code 204) + * or Not Found (status code 404) + * @see AirportApi#deleteCityAssignment + */ + @Override + public ResponseEntity<Void> deleteCityAssignment(Long airportId, Long cityId) { + airportFacade.deleteCityAssignment(airportId, cityId); + return null; } /** @@ -105,7 +146,7 @@ public class AirportController implements AirportApiDelegate { */ @Override public ResponseEntity<AirportDto> getAirportById(Long id) { - return AirportApiDelegate.super.getAirportById(id); + return ResponseEntity.of(airportFacade.findById(id)); } /** @@ -117,49 +158,21 @@ public class AirportController implements AirportApiDelegate { */ @Override public ResponseEntity<List<AirportDto>> getAllAirports() { - return AirportApiDelegate.super.getAllAirports(); + return ResponseEntity.ok(airportFacade.findAll()); } /** - * PUT /api/airports/{id} : Update city by id. + * PUT /api/airports/{id} : Update airport by id. * Updates a airport by id and returns it as a response. * - * @param id (required) - * @param airportDto (required) + * @param id (required) + * @param newAirportDtoRequest (required) * @return OK (status code 200) - * or Input data not correct (status code 400) + * or Input data not correct (status code 400) * @see AirportApi#updateAirport */ @Override - public ResponseEntity<AirportDto> updateAirport(Long id, AirportDto airportDto) { - return AirportApiDelegate.super.updateAirport(id, airportDto); - } - - /** - * PUT /api/airports/{airportId}/arrivingFlights/{arrivingFlightId} : Update assignment of arriving flight to an airport. - * - * @param airportId (required) - * @param arrivingFlightId (required) - * @return OK (status code 200) - * or Input data not correct (status code 400) - * @see AirportApi#updateArrivingFlightAssignment - */ - @Override - public ResponseEntity<AirportDto> updateArrivingFlightAssignment(Long airportId, Long arrivingFlightId) { - return AirportApiDelegate.super.updateArrivingFlightAssignment(airportId, arrivingFlightId); - } - - /** - * PUT /api/airports/{airportId}/departingFlights/{departingFlightId} : Update assignment of departing flight to an airport. - * - * @param airportId (required) - * @param departingFlightId (required) - * @return OK (status code 200) - * or Input data not correct (status code 400) - * @see AirportApi#updateDepartingFlightAssignment - */ - @Override - public ResponseEntity<AirportDto> updateDepartingFlightAssignment(Long airportId, Long departingFlightId) { - return AirportApiDelegate.super.updateDepartingFlightAssignment(airportId, departingFlightId); + public ResponseEntity<AirportDto> updateAirport(Long id, NewAirportDtoRequest newAirportDtoRequest) { + return ResponseEntity.ok(airportFacade.update(id, newAirportDtoRequest)); } } diff --git a/core/src/main/java/cz/muni/fi/pa165/core/rest/CityController.java b/core/src/main/java/cz/muni/fi/pa165/core/rest/CityController.java index 2fd210bdb20c5de3d418eb2ca8618883173f6e09..806a0ad7fb6ba603a4b8830ea635f451771ed95a 100644 --- a/core/src/main/java/cz/muni/fi/pa165/core/rest/CityController.java +++ b/core/src/main/java/cz/muni/fi/pa165/core/rest/CityController.java @@ -2,14 +2,28 @@ package cz.muni.fi.pa165.core.rest; import cz.muni.fi.pa165.core.api.CityApi; import cz.muni.fi.pa165.core.api.CityApiDelegate; +import cz.muni.fi.pa165.core.facade.city.CityFacade; +import cz.muni.fi.pa165.core.facade.country.CountryFacade; import cz.muni.fi.pa165.core.model.CityDto; import cz.muni.fi.pa165.core.model.NewCityDtoRequest; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.RestController; +import java.util.List; + @RestController public class CityController implements CityApiDelegate { + private final CityFacade<Long> cityFacade; + private final CountryFacade<Long> countryFacade; + + @Autowired + public CityController(CityFacade<Long> cityFacade, CountryFacade<Long> countryFacade) { + this.cityFacade = cityFacade; + this.countryFacade = countryFacade; + } + /** * POST /api/cities/{cityId}/countries/{countryId} : Assign country to a city. * @@ -21,7 +35,7 @@ public class CityController implements CityApiDelegate { */ @Override public ResponseEntity<CityDto> assignCountry(Long cityId, Long countryId) { - return CityApiDelegate.super.assignCountry(cityId, countryId); + return ResponseEntity.ok(cityFacade.addCountryAssignment(cityId, countryId)); } /** @@ -35,7 +49,7 @@ public class CityController implements CityApiDelegate { */ @Override public ResponseEntity<CityDto> createCity(NewCityDtoRequest newCityDtoRequest) { - return CityApiDelegate.super.createCity(newCityDtoRequest); + return ResponseEntity.ok(cityFacade.save(newCityDtoRequest)); } /** @@ -48,7 +62,8 @@ public class CityController implements CityApiDelegate { */ @Override public ResponseEntity<Void> deleteCity(Long id) { - return CityApiDelegate.super.deleteCity(id); + cityFacade.deleteById(id); + return null; } /** @@ -62,7 +77,8 @@ public class CityController implements CityApiDelegate { */ @Override public ResponseEntity<Void> deleteCountryAssignment(Long cityId, Long countryId) { - return CityApiDelegate.super.deleteCountryAssignment(cityId, countryId); + cityFacade.deleteCountryAssignment(cityId, countryId); + return null; } /** @@ -75,35 +91,33 @@ public class CityController implements CityApiDelegate { */ @Override public ResponseEntity<CityDto> getCityById(Long id) { - return CityApiDelegate.super.getCityById(id); + return ResponseEntity.of(cityFacade.findById(id)); } /** - * PUT /api/cities/{id} : Update city by id. - * Updates a city by id and returns it as a response. + * GET /api/cities : Get all cities. + * Returns an array of objects representing cities. * - * @param id (required) - * @param cityDto (required) * @return OK (status code 200) - * or Input data not correct (status code 400) - * @see CityApi#updateCity + * @see CityApi#getAllCities */ @Override - public ResponseEntity<CityDto> updateCity(Long id, CityDto cityDto) { - return CityApiDelegate.super.updateCity(id, cityDto); + public ResponseEntity<List<CityDto>> getAllCities() { + return ResponseEntity.ok(cityFacade.findAll()); } /** - * PUT /api/cities/{cityId}/countries/{countryId} : Update assignment of country to a city. + * PUT /api/cities/{id} : Update city by id. + * Updates a city by id and returns it as a response. * - * @param cityId (required) - * @param countryId (required) + * @param id (required) + * @param newCityDtoRequest (required) * @return OK (status code 200) - * or Input data not correct (status code 400) - * @see CityApi#updateCountryAssignment + * or Input data not correct (status code 400) + * @see CityApi#updateCity */ @Override - public ResponseEntity<CityDto> updateCountryAssignment(Long cityId, Long countryId) { - return CityApiDelegate.super.updateCountryAssignment(cityId, countryId); + public ResponseEntity<CityDto> updateCity(Long id, NewCityDtoRequest newCityDtoRequest) { + return ResponseEntity.ok(cityFacade.update(id, newCityDtoRequest)); } } diff --git a/core/src/main/java/cz/muni/fi/pa165/core/rest/CountryController.java b/core/src/main/java/cz/muni/fi/pa165/core/rest/CountryController.java index c2c0a698c97fc5984083f20258272b82809fcf6e..f9d76971ff4d5b3501b67d97dbdc16d3986caf19 100644 --- a/core/src/main/java/cz/muni/fi/pa165/core/rest/CountryController.java +++ b/core/src/main/java/cz/muni/fi/pa165/core/rest/CountryController.java @@ -2,9 +2,10 @@ package cz.muni.fi.pa165.core.rest; import cz.muni.fi.pa165.core.api.CountryApi; import cz.muni.fi.pa165.core.api.CountryApiDelegate; +import cz.muni.fi.pa165.core.facade.country.CountryFacade; import cz.muni.fi.pa165.core.model.CountryDto; import cz.muni.fi.pa165.core.model.NewCountryDtoRequest; -import org.springframework.http.HttpStatus; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.RestController; @@ -13,6 +14,13 @@ import java.util.List; @RestController public class CountryController implements CountryApiDelegate { + private final CountryFacade<Long> countryFacade; + + @Autowired + public CountryController(CountryFacade<Long> countryFacade) { + this.countryFacade = countryFacade; + } + /** * POST /api/countries : Create a new country. * Creates a new country and returns it as a response. @@ -24,11 +32,7 @@ public class CountryController implements CountryApiDelegate { */ @Override public ResponseEntity<CountryDto> createCountry(NewCountryDtoRequest newCountryDtoRequest) { - // Sample implementation just for the purpose of testing Rest endpoint - var countryDto = new CountryDto() - .id(42L) - .name("Uganda"); - return new ResponseEntity<>(countryDto, HttpStatus.CREATED); + return ResponseEntity.ok(countryFacade.save(newCountryDtoRequest)); } /** @@ -41,7 +45,8 @@ public class CountryController implements CountryApiDelegate { */ @Override public ResponseEntity<Void> deleteCountry(Long id) { - return CountryApiDelegate.super.deleteCountry(id); + countryFacade.deleteById(id); + return null; } /** @@ -53,8 +58,7 @@ public class CountryController implements CountryApiDelegate { */ @Override public ResponseEntity<List<CountryDto>> getAllCountries() { - // Sample implementation just for the purpose of testing Rest endpoint - return CountryApiDelegate.super.getAllCountries(); + return ResponseEntity.ok(countryFacade.findAll()); } /** @@ -67,24 +71,21 @@ public class CountryController implements CountryApiDelegate { */ @Override public ResponseEntity<CountryDto> getCountryById(Long id) { - var countryDto = new CountryDto() - .id(4242L) - .name("Fraaaance"); - return ResponseEntity.ok(countryDto); + return ResponseEntity.of(countryFacade.findById(id)); } /** * PUT /api/countries/{id} : Update country by id. * Updates a country by id and returns it as a response. * - * @param id (required) - * @param countryDto (required) + * @param id (required) + * @param newCountryDtoRequest (required) * @return OK (status code 200) - * or Input data not correct (status code 400) + * or Input data not correct (status code 400) * @see CountryApi#updateCountry */ @Override - public ResponseEntity<CountryDto> updateCountry(Long id, CountryDto countryDto) { - return CountryApiDelegate.super.updateCountry(id, countryDto); + public ResponseEntity<CountryDto> updateCountry(Long id, NewCountryDtoRequest newCountryDtoRequest) { + return ResponseEntity.ok(countryFacade.update(id, newCountryDtoRequest)); } } diff --git a/core/src/main/java/cz/muni/fi/pa165/core/rest/DatabaseController.java b/core/src/main/java/cz/muni/fi/pa165/core/rest/DatabaseController.java new file mode 100644 index 0000000000000000000000000000000000000000..a73af3cfbe12620d70a2f1a00f164ea768be39c7 --- /dev/null +++ b/core/src/main/java/cz/muni/fi/pa165/core/rest/DatabaseController.java @@ -0,0 +1,43 @@ +package cz.muni.fi.pa165.core.rest; + +import cz.muni.fi.pa165.core.api.DatabaseApi; +import cz.muni.fi.pa165.core.api.DatabaseApiDelegate; +import cz.muni.fi.pa165.core.data.seed.DatabaseInitializer; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.RestController; + +@RestController +public class DatabaseController implements DatabaseApiDelegate { + + private final DatabaseInitializer databaseInitializer; + + @Autowired + public DatabaseController(DatabaseInitializer databaseInitializer) { + this.databaseInitializer = databaseInitializer; + } + + /** + * POST /api/db/seed : Seeds the predefined data database + * + * @return Created (status code 201) + * @see DatabaseApi#seed + */ + @Override + public ResponseEntity<Void> seed() { + databaseInitializer.seed(); + return ResponseEntity.ok().build(); + } + + /** + * DELETE /api/db/clear : Clears the predefined data database + * + * @return Deleted (status code 204) + * @see DatabaseApi#clear + */ + @Override + public ResponseEntity<Void> clear() { + databaseInitializer.clear(); + return ResponseEntity.ok().build(); + } +} diff --git a/core/src/main/java/cz/muni/fi/pa165/core/rest/FlightController.java b/core/src/main/java/cz/muni/fi/pa165/core/rest/FlightController.java index f36ad554b1f3791375a6337663f1338c6a589b06..b77606628f2c8c04bf7bd964d1f67bdfb8290361 100644 --- a/core/src/main/java/cz/muni/fi/pa165/core/rest/FlightController.java +++ b/core/src/main/java/cz/muni/fi/pa165/core/rest/FlightController.java @@ -2,8 +2,10 @@ package cz.muni.fi.pa165.core.rest; import cz.muni.fi.pa165.core.api.FlightApi; import cz.muni.fi.pa165.core.api.FlightApiDelegate; +import cz.muni.fi.pa165.core.facade.flight.FlightFacade; import cz.muni.fi.pa165.core.model.FlightDto; import cz.muni.fi.pa165.core.model.NewFlightDtoRequest; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.RestController; @@ -12,6 +14,13 @@ import java.util.List; @RestController public class FlightController implements FlightApiDelegate { + private final FlightFacade<Long> flightFacade; + + @Autowired + public FlightController(FlightFacade<Long> flightFacade) { + this.flightFacade = flightFacade; + } + /** * POST /api/flights : Create a new flight. * Creates a new flight and returns it as a response. @@ -23,7 +32,7 @@ public class FlightController implements FlightApiDelegate { */ @Override public ResponseEntity<FlightDto> createFlight(NewFlightDtoRequest newFlightDtoRequest) { - return FlightApiDelegate.super.createFlight(newFlightDtoRequest); + return ResponseEntity.ok(flightFacade.save(newFlightDtoRequest)); } /** @@ -36,7 +45,8 @@ public class FlightController implements FlightApiDelegate { */ @Override public ResponseEntity<Void> deleteFlight(Long id) { - return FlightApiDelegate.super.deleteFlight(id); + flightFacade.deleteById(id); + return null; } /** @@ -48,7 +58,7 @@ public class FlightController implements FlightApiDelegate { */ @Override public ResponseEntity<List<FlightDto>> getAllFlights() { - return FlightApiDelegate.super.getAllFlights(); + return ResponseEntity.ok(flightFacade.findAll()); } /** @@ -61,7 +71,7 @@ public class FlightController implements FlightApiDelegate { */ @Override public ResponseEntity<FlightDto> getFlightById(Long id) { - return FlightApiDelegate.super.getFlightById(id); + return ResponseEntity.of(flightFacade.findById(id)); } /** @@ -76,6 +86,6 @@ public class FlightController implements FlightApiDelegate { */ @Override public ResponseEntity<FlightDto> updateFlight(Long id, NewFlightDtoRequest newFlightDtoRequest) { - return FlightApiDelegate.super.updateFlight(id, newFlightDtoRequest); + return ResponseEntity.ok(flightFacade.update(id, newFlightDtoRequest)); } } diff --git a/core/src/main/java/cz/muni/fi/pa165/core/rest/StewardController.java b/core/src/main/java/cz/muni/fi/pa165/core/rest/StewardController.java index c24bdc0a7bdc783ec10b4ad1d29f686983f8f842..e041698c013dccd342f45211b2bec02453a9b916 100644 --- a/core/src/main/java/cz/muni/fi/pa165/core/rest/StewardController.java +++ b/core/src/main/java/cz/muni/fi/pa165/core/rest/StewardController.java @@ -2,7 +2,9 @@ package cz.muni.fi.pa165.core.rest; import cz.muni.fi.pa165.core.api.StewardApi; import cz.muni.fi.pa165.core.api.StewardApiDelegate; +import cz.muni.fi.pa165.core.facade.steward.StewardFacade; import cz.muni.fi.pa165.core.model.*; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.RestController; @@ -11,6 +13,13 @@ import java.util.List; @RestController public class StewardController implements StewardApiDelegate { + private final StewardFacade<Long> stewardFacade; + + @Autowired + public StewardController(StewardFacade<Long> stewardFacade) { + this.stewardFacade = stewardFacade; + } + /** * POST /api/stewards : Create a new steward. * Creates a new steward and returns it as a response. @@ -22,7 +31,7 @@ public class StewardController implements StewardApiDelegate { */ @Override public ResponseEntity<StewardDto> createSteward(NewStewardDtoRequest newStewardDtoRequest) { - return StewardApiDelegate.super.createSteward(newStewardDtoRequest); + return ResponseEntity.ok(stewardFacade.save(newStewardDtoRequest)); } /** @@ -36,7 +45,7 @@ public class StewardController implements StewardApiDelegate { */ @Override public ResponseEntity<StewardDto> createStewardFlights(Long stewardId, Long flightId) { - return StewardApiDelegate.super.createStewardFlights(stewardId, flightId); + return ResponseEntity.ok(stewardFacade.assignStewardFlight(stewardId, flightId)); } /** @@ -49,7 +58,8 @@ public class StewardController implements StewardApiDelegate { */ @Override public ResponseEntity<Void> deleteSteward(Long id) { - return StewardApiDelegate.super.deleteSteward(id); + stewardFacade.deleteById(id); + return null; } /** @@ -63,7 +73,8 @@ public class StewardController implements StewardApiDelegate { */ @Override public ResponseEntity<Void> deleteStewardFlights(Long stewardId, Long flightId) { - return StewardApiDelegate.super.deleteStewardFlights(stewardId, flightId); + stewardFacade.deleteStewardFlightsAssignment(stewardId, flightId); + return null; } /** @@ -75,7 +86,7 @@ public class StewardController implements StewardApiDelegate { */ @Override public ResponseEntity<List<StewardDto>> getAllStewards() { - return StewardApiDelegate.super.getAllStewards(); + return ResponseEntity.ok(stewardFacade.findAll()); } /** @@ -89,7 +100,7 @@ public class StewardController implements StewardApiDelegate { */ @Override public ResponseEntity<StewardDto> getSteward(Long id) { - return StewardApiDelegate.super.getSteward(id); + return ResponseEntity.of(stewardFacade.findById(id)); } /** @@ -119,20 +130,6 @@ public class StewardController implements StewardApiDelegate { */ @Override public ResponseEntity<StewardDto> updateSteward(Long id, NewStewardDtoRequest newStewardDtoRequest) { - return StewardApiDelegate.super.updateSteward(id, newStewardDtoRequest); - } - - /** - * PUT /api/stewards/{stewardId}/flights/{flightId} : Update assignment of steward to a flight. - * - * @param stewardId (required) - * @param flightId (required) - * @return Response containing a single Steward. (status code 200) - * or Input data not correct (status code 400) - * @see StewardApi#updateStewardFlights - */ - @Override - public ResponseEntity<StewardDto> updateStewardFlights(Long stewardId, Long flightId) { - return StewardApiDelegate.super.updateStewardFlights(stewardId, flightId); + return ResponseEntity.ok(stewardFacade.update(id, newStewardDtoRequest)); } } diff --git a/core/src/main/java/cz/muni/fi/pa165/core/service/airplane/AirplaneService.java b/core/src/main/java/cz/muni/fi/pa165/core/service/airplane/AirplaneService.java new file mode 100644 index 0000000000000000000000000000000000000000..c724f1e16fd54ae0d791a1c76b067f3c77824679 --- /dev/null +++ b/core/src/main/java/cz/muni/fi/pa165/core/service/airplane/AirplaneService.java @@ -0,0 +1,9 @@ +package cz.muni.fi.pa165.core.service.airplane; + +import cz.muni.fi.pa165.core.data.domain.Airplane; +import cz.muni.fi.pa165.core.service.common.BaseService; + +public interface AirplaneService extends BaseService<Airplane, Long> { + + Airplane update(Long id, Airplane newAirplane); +} diff --git a/core/src/main/java/cz/muni/fi/pa165/core/service/airplane/AirplaneServiceImpl.java b/core/src/main/java/cz/muni/fi/pa165/core/service/airplane/AirplaneServiceImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..17d6445a797d8b8d16a4c35b31c654785eb7b333 --- /dev/null +++ b/core/src/main/java/cz/muni/fi/pa165/core/service/airplane/AirplaneServiceImpl.java @@ -0,0 +1,31 @@ +package cz.muni.fi.pa165.core.service.airplane; + +import cz.muni.fi.pa165.core.data.domain.Airplane; +import cz.muni.fi.pa165.core.data.repository.airplane.AirplaneRepository; +import cz.muni.fi.pa165.core.exceptions.ResourceNotFoundException; +import cz.muni.fi.pa165.core.service.common.BaseServiceImpl; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +@Service +public class AirplaneServiceImpl extends BaseServiceImpl<Airplane, Long> implements AirplaneService { + + private final AirplaneRepository airplaneRepository; + + @Autowired + public AirplaneServiceImpl(AirplaneRepository airplaneRepository) { + super(airplaneRepository); + this.airplaneRepository = airplaneRepository; + } + + @Override + public Airplane update(Long id, Airplane newAirplane) { + var entityToUpdate = airplaneRepository.findById(id) + .orElseThrow(ResourceNotFoundException::new); + + entityToUpdate.setName(newAirplane.getName()); + entityToUpdate.setCapacity(newAirplane.getCapacity()); + + return airplaneRepository.save(entityToUpdate); + } +} diff --git a/core/src/main/java/cz/muni/fi/pa165/core/service/airplanetype/AirplaneTypeService.java b/core/src/main/java/cz/muni/fi/pa165/core/service/airplanetype/AirplaneTypeService.java new file mode 100644 index 0000000000000000000000000000000000000000..72a345900acb1b62bcf661204c3b5f0a90b440fe --- /dev/null +++ b/core/src/main/java/cz/muni/fi/pa165/core/service/airplanetype/AirplaneTypeService.java @@ -0,0 +1,13 @@ +package cz.muni.fi.pa165.core.service.airplanetype; + +import cz.muni.fi.pa165.core.data.domain.AirplaneType; +import cz.muni.fi.pa165.core.service.common.BaseService; + +import java.util.Optional; + +public interface AirplaneTypeService extends BaseService<AirplaneType, Long> { + + Optional<AirplaneType> findByName(String name); + + AirplaneType update(Long id, AirplaneType newAirplaneType); +} \ No newline at end of file diff --git a/core/src/main/java/cz/muni/fi/pa165/core/service/airplanetype/AirplaneTypeServiceImpl.java b/core/src/main/java/cz/muni/fi/pa165/core/service/airplanetype/AirplaneTypeServiceImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..a776ef1a4ca59990eb17532e9e2c4005530e4c35 --- /dev/null +++ b/core/src/main/java/cz/muni/fi/pa165/core/service/airplanetype/AirplaneTypeServiceImpl.java @@ -0,0 +1,37 @@ +package cz.muni.fi.pa165.core.service.airplanetype; + +import cz.muni.fi.pa165.core.data.domain.AirplaneType; +import cz.muni.fi.pa165.core.data.repository.airplanetype.AirplaneTypeRepository; +import cz.muni.fi.pa165.core.exceptions.ResourceNotFoundException; +import cz.muni.fi.pa165.core.service.common.BaseServiceImpl; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.Optional; + +@Service +public class AirplaneTypeServiceImpl extends BaseServiceImpl<AirplaneType, Long> implements AirplaneTypeService { + + private final AirplaneTypeRepository airplaneTypeRepository; + + @Autowired + public AirplaneTypeServiceImpl(AirplaneTypeRepository airplaneTypeRepository) { + super(airplaneTypeRepository); + this.airplaneTypeRepository = airplaneTypeRepository; + } + + @Override + public Optional<AirplaneType> findByName(String name) { + return airplaneTypeRepository.findByName(name); + } + + @Override + public AirplaneType update(Long id, AirplaneType newAirplaneType) { + var entityToUpdate = airplaneTypeRepository.findById(id) + .orElseThrow(ResourceNotFoundException::new); + + entityToUpdate.setName(newAirplaneType.getName()); + + return airplaneTypeRepository.save(entityToUpdate); + } +} \ No newline at end of file diff --git a/core/src/main/java/cz/muni/fi/pa165/core/service/airport/AirportService.java b/core/src/main/java/cz/muni/fi/pa165/core/service/airport/AirportService.java new file mode 100644 index 0000000000000000000000000000000000000000..4ae859c70b9aa7b08b54217779f2ea05cab030a2 --- /dev/null +++ b/core/src/main/java/cz/muni/fi/pa165/core/service/airport/AirportService.java @@ -0,0 +1,21 @@ +package cz.muni.fi.pa165.core.service.airport; + +import cz.muni.fi.pa165.core.data.domain.Airport; +import cz.muni.fi.pa165.core.data.domain.City; +import cz.muni.fi.pa165.core.service.common.BaseService; +import org.springframework.stereotype.Service; + +import java.util.List; +import java.util.Optional; + +@Service +public interface AirportService extends BaseService<Airport, Long> { + + Optional<Airport> findByName(String name); + + Optional<Airport> findByCode(String code); + + List<Airport> findByCity(City city); + + Airport update(Long id, Airport updatedAirport); +} diff --git a/core/src/main/java/cz/muni/fi/pa165/core/service/airport/AirportServiceImpl.java b/core/src/main/java/cz/muni/fi/pa165/core/service/airport/AirportServiceImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..881f95850096a1bfc4b84929f092859a8a988a2e --- /dev/null +++ b/core/src/main/java/cz/muni/fi/pa165/core/service/airport/AirportServiceImpl.java @@ -0,0 +1,49 @@ +package cz.muni.fi.pa165.core.service.airport; + +import cz.muni.fi.pa165.core.data.domain.Airport; +import cz.muni.fi.pa165.core.data.domain.City; +import cz.muni.fi.pa165.core.data.repository.airport.AirportRepository; +import cz.muni.fi.pa165.core.service.common.BaseServiceImpl; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.List; +import java.util.Optional; + +@Service +public class AirportServiceImpl extends BaseServiceImpl<Airport, Long> implements AirportService { + + private final AirportRepository airportRepository; + + @Autowired + public AirportServiceImpl(AirportRepository airportRepository) { + super(airportRepository); + this.airportRepository = airportRepository; + } + + @Override + public Optional<Airport> findByName(String name) { + return airportRepository.findByName(name); + } + + @Override + public Optional<Airport> findByCode(String code) { + return airportRepository.findByCode(code); + } + + @Override + public List<Airport> findByCity(City city) { + return airportRepository.findByCity(city); + } + + @Override + public Airport update(Long id, Airport updatedAirport) { + Airport airportToUpdate = airportRepository.findById(id) + .orElseThrow(RuntimeException::new); + + airportToUpdate.setName(updatedAirport.getName()); + airportToUpdate.setCode(updatedAirport.getCode()); + + return airportRepository.save(airportToUpdate); + } +} diff --git a/core/src/main/java/cz/muni/fi/pa165/core/service/city/CityService.java b/core/src/main/java/cz/muni/fi/pa165/core/service/city/CityService.java new file mode 100644 index 0000000000000000000000000000000000000000..663bd787ba0d12d72dbd339333c0117e675db2de --- /dev/null +++ b/core/src/main/java/cz/muni/fi/pa165/core/service/city/CityService.java @@ -0,0 +1,13 @@ +package cz.muni.fi.pa165.core.service.city; + +import cz.muni.fi.pa165.core.data.domain.City; +import cz.muni.fi.pa165.core.service.common.BaseService; + +import java.util.Optional; + +public interface CityService extends BaseService<City, Long> { + + Optional<City> findByName(String name); + + City update(Long id, City updatedCity); +} diff --git a/core/src/main/java/cz/muni/fi/pa165/core/service/city/CityServiceImpl.java b/core/src/main/java/cz/muni/fi/pa165/core/service/city/CityServiceImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..6dc713cfef09d452f79e373494c0b91ea2845b50 --- /dev/null +++ b/core/src/main/java/cz/muni/fi/pa165/core/service/city/CityServiceImpl.java @@ -0,0 +1,37 @@ +package cz.muni.fi.pa165.core.service.city; + +import cz.muni.fi.pa165.core.data.domain.City; +import cz.muni.fi.pa165.core.data.repository.city.CityRepository; +import cz.muni.fi.pa165.core.exceptions.ResourceNotFoundException; +import cz.muni.fi.pa165.core.service.common.BaseServiceImpl; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.Optional; + +@Service +public class CityServiceImpl extends BaseServiceImpl<City, Long> implements CityService { + + private final CityRepository cityRepository; + + @Autowired + public CityServiceImpl(CityRepository cityRepository) { + super(cityRepository); + this.cityRepository = cityRepository; + } + + @Override + public Optional<City> findByName(String name) { + return cityRepository.findByName(name); + } + + @Override + public City update(Long id, City updatedCity) { + City cityToUpdate = cityRepository.findById(id) + .orElseThrow(ResourceNotFoundException::new); + + cityToUpdate.setName(updatedCity.getName()); + + return cityRepository.save(cityToUpdate); + } +} diff --git a/core/src/main/java/cz/muni/fi/pa165/core/service/common/BaseService.java b/core/src/main/java/cz/muni/fi/pa165/core/service/common/BaseService.java new file mode 100644 index 0000000000000000000000000000000000000000..c85567d5c294f5751b878cc9f844e49daaa5e829 --- /dev/null +++ b/core/src/main/java/cz/muni/fi/pa165/core/service/common/BaseService.java @@ -0,0 +1,26 @@ +package cz.muni.fi.pa165.core.service.common; + +import java.util.List; +import java.util.Optional; + +/** + * BaseService for common CRUD operations + * + * @param <E> Entity + * @param <K> Key + * @author martinslovik + */ +public interface BaseService<E, K> { + + E save(E entity); + + Optional<E> findById(K id); + + List<E> findAll(); + + void deleteById(K id); + + void deleteAll(); + + boolean existsById(K id); +} diff --git a/core/src/main/java/cz/muni/fi/pa165/core/service/common/BaseServiceImpl.java b/core/src/main/java/cz/muni/fi/pa165/core/service/common/BaseServiceImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..d937a14a4659db714530cac1939f8d54ca677c9a --- /dev/null +++ b/core/src/main/java/cz/muni/fi/pa165/core/service/common/BaseServiceImpl.java @@ -0,0 +1,60 @@ +package cz.muni.fi.pa165.core.service.common; + +import cz.muni.fi.pa165.core.data.domain.common.DomainEntity; +import cz.muni.fi.pa165.core.data.repository.common.BaseRepository; +import jakarta.validation.ConstraintViolationException; +import jakarta.validation.Validator; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.List; +import java.util.Optional; + + +@Service +public abstract class BaseServiceImpl<E extends DomainEntity, K> implements BaseService<E, K> { + + private final BaseRepository<E, K> repository; + + @Autowired + private Validator validator; + + @Autowired + protected BaseServiceImpl(BaseRepository<E, K> repository) { + this.repository = repository; + } + + @Override + public E save(E entity) { + var violations = validator.validate(entity); + if (!violations.isEmpty()) { + throw new ConstraintViolationException(violations); + } + return repository.save(entity); + } + + @Override + public Optional<E> findById(K id) { + return repository.findById(id); + } + + @Override + public List<E> findAll() { + return (List<E>) repository.findAll(); + } + + @Override + public void deleteById(K id) { + repository.deleteById(id); + } + + @Override + public void deleteAll() { + repository.deleteAll(); + } + + @Override + public boolean existsById(K id) { + return repository.existsById(id); + } +} diff --git a/core/src/main/java/cz/muni/fi/pa165/core/service/country/CountryService.java b/core/src/main/java/cz/muni/fi/pa165/core/service/country/CountryService.java new file mode 100644 index 0000000000000000000000000000000000000000..bbe8b5f4c73f477c87e341b86acdf6485fa49016 --- /dev/null +++ b/core/src/main/java/cz/muni/fi/pa165/core/service/country/CountryService.java @@ -0,0 +1,13 @@ +package cz.muni.fi.pa165.core.service.country; + +import cz.muni.fi.pa165.core.data.domain.Country; +import cz.muni.fi.pa165.core.service.common.BaseService; + +import java.util.Optional; + +public interface CountryService extends BaseService<Country, Long> { + + Optional<Country> findByName(String name); + + Country update(Long id, Country updatedCountry); +} diff --git a/core/src/main/java/cz/muni/fi/pa165/core/service/country/CountryServiceImpl.java b/core/src/main/java/cz/muni/fi/pa165/core/service/country/CountryServiceImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..ca3ce82c1a9561bbd9c3acf09bdaabf1eb37eac8 --- /dev/null +++ b/core/src/main/java/cz/muni/fi/pa165/core/service/country/CountryServiceImpl.java @@ -0,0 +1,40 @@ +package cz.muni.fi.pa165.core.service.country; + +import cz.muni.fi.pa165.core.data.domain.Country; +import cz.muni.fi.pa165.core.data.repository.country.CountryRepository; +import cz.muni.fi.pa165.core.exceptions.ResourceNotFoundException; +import cz.muni.fi.pa165.core.service.common.BaseServiceImpl; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.Optional; + +@Service +public class CountryServiceImpl extends BaseServiceImpl<Country, Long> implements CountryService { + + private final CountryRepository countryRepository; + + @Autowired + public CountryServiceImpl(CountryRepository countryRepository) { + super(countryRepository); + this.countryRepository = countryRepository; + } + + @Transactional + @Override + public Optional<Country> findByName(String name) { + return countryRepository.findByName(name); + } + + @Transactional + @Override + public Country update(Long id, Country updatedCountry) { + Country countryToUpdate = countryRepository.findById(id) + .orElseThrow(() -> new ResourceNotFoundException("Country with id " + id + " not found.")); + + countryToUpdate.setName(updatedCountry.getName()); + + return countryRepository.save(countryToUpdate); + } +} diff --git a/core/src/main/java/cz/muni/fi/pa165/core/service/flight/FlightService.java b/core/src/main/java/cz/muni/fi/pa165/core/service/flight/FlightService.java new file mode 100644 index 0000000000000000000000000000000000000000..3a14ff88255f23d97f3dd633e81b12c39117a601 --- /dev/null +++ b/core/src/main/java/cz/muni/fi/pa165/core/service/flight/FlightService.java @@ -0,0 +1,13 @@ +package cz.muni.fi.pa165.core.service.flight; + +import cz.muni.fi.pa165.core.data.domain.Flight; +import cz.muni.fi.pa165.core.service.common.BaseService; + +import java.util.Optional; + +public interface FlightService extends BaseService<Flight, Long> { + + Optional<Flight> findByIdWithStewards(Long id); + + Flight update(Long id, Flight newFlight); +} diff --git a/core/src/main/java/cz/muni/fi/pa165/core/service/flight/FlightServiceImpl.java b/core/src/main/java/cz/muni/fi/pa165/core/service/flight/FlightServiceImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..abcc02c467237906b8608dac1b3196c9e448a149 --- /dev/null +++ b/core/src/main/java/cz/muni/fi/pa165/core/service/flight/FlightServiceImpl.java @@ -0,0 +1,38 @@ +package cz.muni.fi.pa165.core.service.flight; + +import cz.muni.fi.pa165.core.data.domain.Flight; +import cz.muni.fi.pa165.core.data.repository.flight.FlightRepository; +import cz.muni.fi.pa165.core.exceptions.ResourceNotFoundException; +import cz.muni.fi.pa165.core.service.common.BaseServiceImpl; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.Optional; + +@Service +public class FlightServiceImpl extends BaseServiceImpl<Flight, Long> implements FlightService { + + private final FlightRepository flightRepository; + + @Autowired + public FlightServiceImpl(FlightRepository flightRepository) { + super(flightRepository); + this.flightRepository = flightRepository; + } + + @Override + public Optional<Flight> findByIdWithStewards(Long id) { + return flightRepository.findByIdWithStewards(id); + } + + @Override + public Flight update(Long id, Flight newFlight) { + var entityToUpdate = flightRepository.findById(id) + .orElseThrow(ResourceNotFoundException::new); + + entityToUpdate.setDepartureTime(newFlight.getDepartureTime()); + entityToUpdate.setArrivalTime(newFlight.getArrivalTime()); + + return flightRepository.save(entityToUpdate); + } +} diff --git a/core/src/main/java/cz/muni/fi/pa165/core/service/steward/StewardService.java b/core/src/main/java/cz/muni/fi/pa165/core/service/steward/StewardService.java new file mode 100644 index 0000000000000000000000000000000000000000..9451f8741c179eff2c55db947f01ccff96930b1a --- /dev/null +++ b/core/src/main/java/cz/muni/fi/pa165/core/service/steward/StewardService.java @@ -0,0 +1,24 @@ +package cz.muni.fi.pa165.core.service.steward; + +import cz.muni.fi.pa165.core.data.domain.Flight; +import cz.muni.fi.pa165.core.data.domain.FlightSteward; +import cz.muni.fi.pa165.core.data.domain.Steward; +import cz.muni.fi.pa165.core.service.common.BaseService; + +import java.util.Optional; + +public interface StewardService extends BaseService<Steward, Long> { + + /** + * Saves the assignment of Steward to a Flight + * @param flightSteward Flight<->Steward connection table + * @return FlightSteward connection table stored in DB + */ + FlightSteward saveFlightStewards(FlightSteward flightSteward); + + void deleteFlightStewards(Steward steward, Flight flight); + + Optional<Steward> findByIdWithFlights(Long id); + + Steward update(Long id, Steward newSteward); +} diff --git a/core/src/main/java/cz/muni/fi/pa165/core/service/steward/StewardServiceImpl.java b/core/src/main/java/cz/muni/fi/pa165/core/service/steward/StewardServiceImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..110165813d83a06ba5f16960c5744b11d2edf925 --- /dev/null +++ b/core/src/main/java/cz/muni/fi/pa165/core/service/steward/StewardServiceImpl.java @@ -0,0 +1,55 @@ +package cz.muni.fi.pa165.core.service.steward; + +import cz.muni.fi.pa165.core.data.domain.Flight; +import cz.muni.fi.pa165.core.data.domain.FlightSteward; +import cz.muni.fi.pa165.core.data.domain.Steward; +import cz.muni.fi.pa165.core.data.repository.flightsteward.FlightStewardRepository; +import cz.muni.fi.pa165.core.data.repository.steward.StewardRepository; +import cz.muni.fi.pa165.core.exceptions.ResourceNotFoundException; +import cz.muni.fi.pa165.core.service.common.BaseServiceImpl; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.Optional; + +@Service +public class StewardServiceImpl extends BaseServiceImpl<Steward, Long> implements StewardService { + + private final StewardRepository stewardRepository; + private final FlightStewardRepository flightStewardRepository; + + @Autowired + public StewardServiceImpl(StewardRepository stewardRepository, FlightStewardRepository flightStewardRepository) { + super(stewardRepository); + this.stewardRepository = stewardRepository; + this.flightStewardRepository = flightStewardRepository; + } + + @Override + public FlightSteward saveFlightStewards(FlightSteward flightSteward) { + return flightStewardRepository.save(flightSteward); + } + + @Override + public Optional<Steward> findByIdWithFlights(Long id) { + return stewardRepository.findByIdWithFlights(id); + } + + @Override + public void deleteFlightStewards(Steward steward, Flight flight) { + var foundFlightStewards = flightStewardRepository.findByStewardAndFlight(steward, flight) + .orElseThrow(ResourceNotFoundException::new); + flightStewardRepository.delete(foundFlightStewards); + } + + @Override + public Steward update(Long id, Steward newSteward) { + var entityToUpdate = stewardRepository.findById(id) + .orElseThrow(ResourceNotFoundException::new); + + entityToUpdate.setFirstName(newSteward.getFirstName()); + entityToUpdate.setLastName(newSteward.getLastName()); + + return stewardRepository.save(entityToUpdate); + } +} diff --git a/core/src/main/java/cz/muni/fi/pa165/core/validation/GpsLocationValidator.java b/core/src/main/java/cz/muni/fi/pa165/core/validation/GpsLocationValidator.java new file mode 100644 index 0000000000000000000000000000000000000000..fd944f9e5217483de649969a936a5ce14055a128 --- /dev/null +++ b/core/src/main/java/cz/muni/fi/pa165/core/validation/GpsLocationValidator.java @@ -0,0 +1,42 @@ +package cz.muni.fi.pa165.core.validation; + +import cz.muni.fi.pa165.core.exceptions.InvalidGpsLocationException; +import org.springframework.stereotype.Component; + +@Component +public class GpsLocationValidator { + + private static final double MIN_LATITUDE = -90.0; + private static final double MAX_LATITUDE = 90.0; + private static final double MIN_LONGITUDE = -180.0; + private static final double MAX_LONGITUDE = 180.0; + + public void validate(double latitude, double longitude) { + if (!validLatitude(latitude)) { + throw new InvalidGpsLocationException( + formatExceptionMessage("latitude", MIN_LATITUDE, MAX_LATITUDE, latitude) + ); + } + if (!validLongitude(longitude)) { + throw new InvalidGpsLocationException( + formatExceptionMessage("longitude", MIN_LONGITUDE, MAX_LONGITUDE, longitude) + ); + } + } + + private static boolean validLatitude(double latitude) { + return MIN_LATITUDE <= latitude && latitude <= MAX_LATITUDE; + } + + private static boolean validLongitude(double longitude) { + return MIN_LONGITUDE <= longitude && longitude <= MAX_LONGITUDE; + } + + private String formatExceptionMessage(String indicator, + double lowerBound, + double upperBound, + double indicatorsValue) { + return String.format("Invalid value of %s given: expecting value to be from range [%.2f, %.2f], but got %.2f" + .formatted(indicator, lowerBound, upperBound, indicatorsValue)); + } +} diff --git a/core/src/main/resources/application.yml b/core/src/main/resources/application.yml index 1174f8ddf871f57a787de9f2880346e620d27cbf..80d02907e8687ed9b94a764025a1871a44a9aebf 100644 --- a/core/src/main/resources/application.yml +++ b/core/src/main/resources/application.yml @@ -1,8 +1,42 @@ spring: + h2: + console: + enabled: true + path: /h2 datasource: - url: jdbc:h2:mem:exampleDb + url: jdbc:h2:mem:coreDb username: sa password: password driverClassName: org.h2.Driver jpa: database-platform: org.hibernate.dialect.H2Dialect + hibernate: + ddl-auto: update + security: + oauth2: + resourceserver: + opaque-token: + introspection-uri: https://oidc.muni.cz/oidc/introspect + # Martin Kuba's testing resource server + client-id: d57b3a8f-156e-46de-9f27-39c4daee05e1 + client-secret: fa228ebc-4d54-4cda-901e-4d6287f8b1652a9c9c44-73c9-4502-973f-bcdb4a8ec96a +management: + endpoints: + web: + exposure: + include: 'info,health,metrics,prometheus' + info: + env: + enabled: true + endpoint: + health: + show-details: always + show-components: always + probes: + enabled: true +info: + app: + encoding: 'UTF-8' + java: + source: '17' + target: '17' \ No newline at end of file diff --git a/core/src/test/java/cz/muni/fi/pa165/core/CoreApplicationIT.java b/core/src/test/java/cz/muni/fi/pa165/core/CoreApplicationIT.java index f4ed9d9e4482dab96d99f2845e432d638f28d49d..957bdf30c80755f447c09612ec97b964e7ca279c 100644 --- a/core/src/test/java/cz/muni/fi/pa165/core/CoreApplicationIT.java +++ b/core/src/test/java/cz/muni/fi/pa165/core/CoreApplicationIT.java @@ -1,18 +1,25 @@ package cz.muni.fi.pa165.core; import com.fasterxml.jackson.databind.ObjectMapper; +import cz.muni.fi.pa165.core.facade.country.CountryFacade; import cz.muni.fi.pa165.core.model.CountryDto; import cz.muni.fi.pa165.core.model.NewCountryDtoRequest; import org.junit.jupiter.api.Test; +import org.mockito.Mockito; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.http.MediaType; import org.springframework.test.web.servlet.MockMvc; +import java.util.Optional; + import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; @@ -22,7 +29,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers. * Integration tests. Run by "maven verify". */ @SpringBootTest -@AutoConfigureMockMvc +@AutoConfigureMockMvc(addFilters = false) class CoreApplicationIT { private static final Logger log = LoggerFactory.getLogger(CoreApplicationIT.class); @@ -33,21 +40,30 @@ class CoreApplicationIT { @Autowired ObjectMapper objectMapper; + @MockBean + private CountryFacade<Long> mockCountryFacade; + @Test void createCountryTest() throws Exception { log.debug("createCountryTest() running"); - var expectedId = 42L; - var expectedName = "Uganda"; - var newCountryDtoRequest = new NewCountryDtoRequest(); - newCountryDtoRequest.setName("Canadaa"); + final long expectedId = 42; + final String expectedName = "Uganda"; + + var request = new NewCountryDtoRequest().name(expectedName); + var expectedResponse = new CountryDto() + .id(expectedId) + .name(expectedName); + + Mockito.when(mockCountryFacade.save(any(NewCountryDtoRequest.class))) + .thenReturn(expectedResponse); var response = mockMvc.perform( - post("/api/countries") - .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(newCountryDtoRequest)) + post("/api/countries") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request)) ) - .andExpect(status().isCreated()) + .andExpect(status().isOk()) .andExpect(jsonPath("$.id").value(expectedId)) .andExpect(jsonPath("$.name").value(expectedName)) .andReturn().getResponse().getContentAsString(); @@ -65,7 +81,14 @@ class CoreApplicationIT { var expectedId = 4242L; var expectedName = "Fraaaance"; - var response = mockMvc.perform(get("/api/countries/42")) + var expectedResponse = new CountryDto() + .id(expectedId) + .name(expectedName); + + Mockito.when(mockCountryFacade.findById(eq(expectedId))) + .thenReturn(Optional.of(expectedResponse)); + + var response = mockMvc.perform(get("/api/countries/4242")) .andExpect(status().isOk()) .andExpect(jsonPath("$.id").value(expectedId)) .andExpect(jsonPath("$.name").value(expectedName)) diff --git a/core/src/test/java/cz/muni/fi/pa165/core/mapper/AirplaneMapperTest.java b/core/src/test/java/cz/muni/fi/pa165/core/mapper/AirplaneMapperTest.java new file mode 100644 index 0000000000000000000000000000000000000000..79a04b5442a328a86a7213f72ca97cf13f93b2e1 --- /dev/null +++ b/core/src/test/java/cz/muni/fi/pa165/core/mapper/AirplaneMapperTest.java @@ -0,0 +1,55 @@ +package cz.muni.fi.pa165.core.mapper; + +import cz.muni.fi.pa165.core.data.domain.Airplane; +import cz.muni.fi.pa165.core.data.domain.AirplaneType; +import cz.muni.fi.pa165.core.model.AirplaneDto; +import cz.muni.fi.pa165.core.model.NewAirplaneDtoRequest; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import static org.assertj.core.api.Assertions.assertThat; + +@SpringBootTest +class AirplaneMapperTest { + + @Autowired + private AirplaneMapper airplaneMapper; + + @Test + void toEntityFromNewRequest() { + var newAirplaneDtoRequest = new NewAirplaneDtoRequest(); + newAirplaneDtoRequest.setName("DL7132"); + newAirplaneDtoRequest.setCapacity(191); + + Airplane airplane = airplaneMapper.toEntityFromNewRequest(newAirplaneDtoRequest); + + assertThat(airplane.getName()) + .isEqualTo(newAirplaneDtoRequest.getName()); + assertThat(airplane.getCapacity()) + .isEqualTo(newAirplaneDtoRequest.getCapacity()); + } + + @Test + void toDto() { + Airplane airplane = new Airplane(); + airplane.setId(1L); + airplane.setName("KL4242"); + airplane.setCapacity(210); + var airplaneType = new AirplaneType(); + airplaneType.setId(81L); + airplaneType.setName("Boeing 777-Max"); + airplane.setType(airplaneType); + + AirplaneDto airplaneDto = airplaneMapper.toDto(airplane); + + assertThat(airplaneDto.getId()) + .isEqualTo(airplane.getId()); + assertThat(airplaneDto.getName()) + .isEqualTo(airplane.getName()); + assertThat(airplaneDto.getCapacity()) + .isEqualTo(airplane.getCapacity()); + assertThat(airplaneDto.getTypeId()) + .isEqualTo(airplane.getType().getId()); + } +} \ No newline at end of file diff --git a/core/src/test/java/cz/muni/fi/pa165/core/mapper/AirplaneTypeMapperTest.java b/core/src/test/java/cz/muni/fi/pa165/core/mapper/AirplaneTypeMapperTest.java new file mode 100644 index 0000000000000000000000000000000000000000..683b1129a2a9528c473b0c5b565a2787e0b1f2c8 --- /dev/null +++ b/core/src/test/java/cz/muni/fi/pa165/core/mapper/AirplaneTypeMapperTest.java @@ -0,0 +1,67 @@ +package cz.muni.fi.pa165.core.mapper; + +import cz.muni.fi.pa165.core.data.domain.Airplane; +import cz.muni.fi.pa165.core.data.domain.AirplaneType; +import cz.muni.fi.pa165.core.model.AirplaneTypeDto; +import cz.muni.fi.pa165.core.model.NewAirplaneTypeDtoRequest; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +@SpringBootTest +class AirplaneTypeMapperTest { + + @Autowired + private AirplaneTypeMapper airplaneTypeMapper; + + @Test + void toEntityFromNewRequest() { + var newAirplaneTypeDtoRequest = new NewAirplaneTypeDtoRequest(); + newAirplaneTypeDtoRequest.setName("A320"); + + AirplaneType airplaneType = airplaneTypeMapper.toEntityFromNewRequest(newAirplaneTypeDtoRequest); + + assertThat(airplaneType.getName()) + .isEqualTo(newAirplaneTypeDtoRequest.getName()); + } + + @Test + void toDto() { + var airplaneType = new AirplaneType(); + airplaneType.setId(1L); + airplaneType.setName("A320"); + airplaneType.setAirplanes(createAirplanes()); + + AirplaneTypeDto airplaneTypeDto = airplaneTypeMapper.toDto(airplaneType); + + assertThat(airplaneTypeDto.getId()) + .isEqualTo(airplaneType.getId()); + assertThat(airplaneTypeDto.getName()) + .isEqualTo(airplaneType.getName()); + assertThat(airplaneTypeDto.getAirplanesIds()) + .hasSameSizeAs(airplaneType.getAirplanes()); + for (int i = 0; i < airplaneTypeDto.getAirplanesIds().size(); i++) { + assertThat(airplaneTypeDto.getAirplanesIds().get(i)) + .isEqualTo(airplaneType.getAirplanes().get(i).getId()); + } + } + + @Test + void getAirplanesTypeId() { + Long airplaneTypeId = airplaneTypeMapper.getAirplanesTypeId(null); + + assertThat(airplaneTypeId) + .isZero(); + } + + private List<Airplane> createAirplanes() { + var a320Airplane = new Airplane(); + a320Airplane.setId(42L); + a320Airplane.setName("DL123"); + return List.of(a320Airplane); + } +} \ No newline at end of file diff --git a/core/src/test/java/cz/muni/fi/pa165/core/mapper/AirportMapperTest.java b/core/src/test/java/cz/muni/fi/pa165/core/mapper/AirportMapperTest.java new file mode 100644 index 0000000000000000000000000000000000000000..393692fafc45605da70022c7dce7eefd598d0e20 --- /dev/null +++ b/core/src/test/java/cz/muni/fi/pa165/core/mapper/AirportMapperTest.java @@ -0,0 +1,120 @@ +package cz.muni.fi.pa165.core.mapper; + +import cz.muni.fi.pa165.core.data.domain.Airport; +import cz.muni.fi.pa165.core.data.domain.City; +import cz.muni.fi.pa165.core.data.domain.Flight; +import cz.muni.fi.pa165.core.model.AirportDto; +import cz.muni.fi.pa165.core.model.GPSLocationDto; +import cz.muni.fi.pa165.core.model.NewAirportDtoRequest; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +@SpringBootTest +class AirportMapperTest { + + @Autowired + private AirportMapper airportMapper; + + @Test + void toEntityFromNewRequest() { + var newAirportDtoRequest = new NewAirportDtoRequest(); + newAirportDtoRequest.setName("Hartsfield–Jackson Atlanta International Airport"); + newAirportDtoRequest.setCode("ATL"); + var gpsLocation = new GPSLocationDto(); + gpsLocation.setLatitude(33.64); + gpsLocation.setLongitude(-84.42); + newAirportDtoRequest.setLocation(gpsLocation); + + Airport airport = airportMapper.toEntityFromNewRequest(newAirportDtoRequest); + + assertThat(airport.getName()) + .isEqualTo(newAirportDtoRequest.getName()); + assertThat(airport.getCode()) + .isEqualTo(newAirportDtoRequest.getCode()); + assertThat(airport.getLatitude()) + .isEqualTo(newAirportDtoRequest.getLocation().getLatitude()); + assertThat(airport.getLongitude()) + .isEqualTo(newAirportDtoRequest.getLocation().getLongitude()); + } + + @Test + void toDto() { + Airport jacksonAtlantaAirport = new Airport(); + jacksonAtlantaAirport.setId(42L); + jacksonAtlantaAirport.setName("Hartsfield–Jackson Atlanta International Airport"); + jacksonAtlantaAirport.setCode("ATL"); + var atlanta = new City(); + atlanta.setId(2023L); + atlanta.setName("Atlanta"); + jacksonAtlantaAirport.setCity(atlanta); + var jacksonAtlantaAirportLatitude = 33.64; + var jacksonAtlantaAirportLongitude = -84.41; + jacksonAtlantaAirport.setLatitude(jacksonAtlantaAirportLatitude); + jacksonAtlantaAirport.setLongitude(jacksonAtlantaAirportLongitude); + jacksonAtlantaAirport.setArrivingFlights(createArrivingFlights()); + jacksonAtlantaAirport.setDepartingFlights(createDepartingFlights()); + + AirportDto airportDto = airportMapper.toDto(jacksonAtlantaAirport); + + assertThat(airportDto.getId()) + .isEqualTo(jacksonAtlantaAirport.getId()); + assertThat(airportDto.getName()) + .isEqualTo(jacksonAtlantaAirport.getName()); + assertThat(airportDto.getCode()) + .isEqualTo(jacksonAtlantaAirport.getCode()); + assertThat(airportDto.getCityId()) + .isEqualTo(jacksonAtlantaAirport.getCity().getId()); + assertThat(airportDto.getLocation().getLatitude()) + .isEqualTo(jacksonAtlantaAirport.getLatitude()); + assertThat(airportDto.getLocation().getLongitude()) + .isEqualTo(jacksonAtlantaAirport.getLongitude()); + assertThat(airportDto.getArrivingFlightsIds()) + .hasSameSizeAs(jacksonAtlantaAirport.getArrivingFlights()); + for (int i = 0; i < airportDto.getArrivingFlightsIds().size(); i++) { + assertThat(airportDto.getArrivingFlightsIds().get(i)) + .isEqualTo(jacksonAtlantaAirport.getArrivingFlights().get(i).getId()); + } + assertThat(airportDto.getDepartingFlightsIds()) + .hasSameSizeAs(jacksonAtlantaAirport.getDepartingFlights()); + for (int i = 0; i < airportDto.getDepartingFlightsIds().size(); i++) { + assertThat(airportDto.getDepartingFlightsIds().get(i)) + .isEqualTo(jacksonAtlantaAirport.getDepartingFlights().get(i).getId()); + } + } + + @Test + void getCityId() { + Long cityId = airportMapper.getCityId(null); + + assertThat(cityId) + .isZero(); + } + + private List<Flight> createArrivingFlights() { + var delta700 = new Flight(); + delta700.setId(701L); + + var ke7282 = new Flight(); + ke7282.setId(1L); + + return List.of(delta700, ke7282); + } + + private List<Flight> createDepartingFlights() { + var cv6101 = new Flight(); + cv6101.setId(13L); + + var ac7258 = new Flight(); + ac7258.setId(72L); + + var f91551 = new Flight(); + f91551.setId(91L); + + return List.of(cv6101, ac7258, f91551); + } +} \ No newline at end of file diff --git a/core/src/test/java/cz/muni/fi/pa165/core/mapper/CityMapperTest.java b/core/src/test/java/cz/muni/fi/pa165/core/mapper/CityMapperTest.java new file mode 100644 index 0000000000000000000000000000000000000000..ce1a9405944ffddb1ab05b7a51509df678679900 --- /dev/null +++ b/core/src/test/java/cz/muni/fi/pa165/core/mapper/CityMapperTest.java @@ -0,0 +1,86 @@ +package cz.muni.fi.pa165.core.mapper; + +import cz.muni.fi.pa165.core.data.domain.Airport; +import cz.muni.fi.pa165.core.data.domain.City; +import cz.muni.fi.pa165.core.data.domain.Country; +import cz.muni.fi.pa165.core.model.CityDto; +import cz.muni.fi.pa165.core.model.NewCityDtoRequest; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +@SpringBootTest +class CityMapperTest { + + @Autowired + private CityMapper cityMapper; + + @Test + void toEntityFromNewRequest() { + var newCityDtoRequest = new NewCityDtoRequest(); + newCityDtoRequest.setName("Winnipeg"); + + City city = cityMapper.toEntityFromNewRequest(newCityDtoRequest); + + assertThat(city.getName()) + .isEqualTo(newCityDtoRequest.getName()); + } + + @Test + void toDto() { + var city = new City(); + city.setId(1L); + city.setName("Winnipeg"); + var canada = new Country(); + canada.setId(1L); + canada.setName("Canada"); + city.setCountry(canada); + city.setAirports(createAirports()); + + CityDto cityDto = cityMapper.toDto(city); + + assertThat(cityDto.getId()) + .isEqualTo(city.getId()); + assertThat(cityDto.getName()) + .isEqualTo(city.getName()); + assertThat(cityDto.getCountryId()) + .isEqualTo(city.getCountry().getId()); + assertThat(cityDto.getAirportsIds()) + .hasSameSizeAs(city.getAirports()); + for (int i = 0; i < cityDto.getAirportsIds().size(); i++) { + assertThat(cityDto.getAirportsIds().get(i)) + .isEqualTo(city.getAirports().get(i).getId()); + } + } + + @Test + void getCountryId() { + Long countryId = cityMapper.getCountryId(null); + + assertThat(countryId) + .isZero(); + } + + private List<Airport> createAirports() { + var yvr = new Airport(); + yvr.setId(42L); + yvr.setCode("YVR"); + yvr.setName("Vancouver International Airport"); + + var yvz = new Airport(); + yvz.setId(13L); + yvz.setCode("YVZ"); + yvz.setName("Toronto Pearson International Airport"); + + var yyc = new Airport(); + yyc.setId(181L); + yyc.setCode("YYC"); + yyc.setName("Calgary International Airport"); + + return List.of(yvr, yvz, yyc); + } +} \ No newline at end of file diff --git a/core/src/test/java/cz/muni/fi/pa165/core/mapper/CountryMapperTest.java b/core/src/test/java/cz/muni/fi/pa165/core/mapper/CountryMapperTest.java new file mode 100644 index 0000000000000000000000000000000000000000..23036ea63c529bdd0be83a75ada2ca1c5b8eaccd --- /dev/null +++ b/core/src/test/java/cz/muni/fi/pa165/core/mapper/CountryMapperTest.java @@ -0,0 +1,61 @@ +package cz.muni.fi.pa165.core.mapper; + +import cz.muni.fi.pa165.core.data.domain.City; +import cz.muni.fi.pa165.core.data.domain.Country; +import cz.muni.fi.pa165.core.model.CountryDto; +import cz.muni.fi.pa165.core.model.NewCountryDtoRequest; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +@SpringBootTest +class CountryMapperTest { + + @Autowired + private CountryMapper countryMapper; + + @Test + void toEntityFromNewRequest() { + var newCountryDtoRequest = new NewCountryDtoRequest(); + newCountryDtoRequest.setName("Canada"); + + Country country = countryMapper.toEntityFromNewRequest(newCountryDtoRequest); + + assertThat(country.getName()) + .isEqualTo(newCountryDtoRequest.getName()); + } + + @Test + void toDto() { + var country = new Country(); + country.setId(1L); + country.setName("Canada"); + City toronto = createCity(1L, "Toronto"); + City ottawa = createCity(2L, "Ottawa"); + country.setCities(List.of(toronto, ottawa)); + + CountryDto countryDto = countryMapper.toDto(country); + + assertThat(countryDto.getId()) + .isEqualTo(country.getId()); + assertThat(countryDto.getName()) + .isEqualTo(country.getName()); + assertThat(countryDto.getCitiesIds()) + .hasSameSizeAs(country.getCities()); + for (int i = 0; i < countryDto.getCitiesIds().size(); i++) { + assertThat(countryDto.getCitiesIds().get(i)) + .isEqualTo(country.getCities().get(i).getId()); + } + } + + private static City createCity(Long id, String name) { + var city = new City(); + city.setId(id); + city.setName(name); + return city; + } +} \ No newline at end of file diff --git a/core/src/test/java/cz/muni/fi/pa165/core/service/airplane/AirplaneServiceTest.java b/core/src/test/java/cz/muni/fi/pa165/core/service/airplane/AirplaneServiceTest.java new file mode 100644 index 0000000000000000000000000000000000000000..0b4a1912b66e9353ddd90455d9b522caff868ab3 --- /dev/null +++ b/core/src/test/java/cz/muni/fi/pa165/core/service/airplane/AirplaneServiceTest.java @@ -0,0 +1,97 @@ +package cz.muni.fi.pa165.core.service.airplane; + +import cz.muni.fi.pa165.core.data.domain.Airplane; +import cz.muni.fi.pa165.core.data.domain.AirplaneType; +import cz.muni.fi.pa165.core.data.repository.airplane.AirplaneRepository; +import cz.muni.fi.pa165.core.data.repository.airplanetype.AirplaneTypeRepository; +import cz.muni.fi.pa165.core.exceptions.ResourceNotFoundException; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; + +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.when; + +@SpringBootTest +class AirplaneServiceTest { + + @Autowired + private AirplaneService airplaneService; + + @MockBean + private AirplaneRepository airplaneRepository; + + @MockBean + private AirplaneTypeRepository airplaneTypeRepository; + + private static final Airplane airplane = new Airplane(); + private static final AirplaneType airplaneType = new AirplaneType(); + + @BeforeAll + static void beforeAll() { + airplane.setId(42L); + airplane.setName("MH370"); + airplaneType.setId(42L); + airplaneType.setName("Boeing 777-200ER"); + airplane.setType(airplaneType); + airplane.setCapacity(239); + } + + @Test + void updateWhenPresent() { + Airplane airplaneToUpdate = new Airplane(); + airplaneToUpdate.setId(1L); + airplaneToUpdate.setName("MH17"); + + when(airplaneRepository.findById(1L)) + .thenReturn(Optional.of(airplaneToUpdate)); + when(airplaneTypeRepository.findById(42L)) + .thenReturn(Optional.of(airplaneType)); + when(airplaneRepository.save(any(Airplane.class))) + .thenReturn(airplane); + + Airplane actualAirplane = airplaneService.update(1L, airplaneToUpdate); + assertThat(actualAirplane.getId()).isEqualTo(42L); + assertThat(actualAirplane.getName()).isEqualTo(airplane.getName()); + assertThat(actualAirplane.getType()).isEqualTo(airplaneType); + assertThat(actualAirplane.getCapacity()).isEqualTo(airplane.getCapacity()); + } + + @Test + void updateWhenNotPresent() { + Airplane airplaneToUpdate = new Airplane(); + airplaneToUpdate.setId(-1L); + airplaneToUpdate.setName("MH17"); + airplaneToUpdate.setCapacity(298); + + when(airplaneRepository.save(any(Airplane.class))) + .thenReturn(airplane); + + assertThrows(ResourceNotFoundException.class, () -> airplaneService.update(-1L, airplaneToUpdate)); + } + + @Test + void save() { + when(airplaneRepository.save(any(Airplane.class))) + .thenReturn(airplane); + + Airplane actualAirplane = airplaneService.save(airplane); + + verify(airplaneRepository, times(1)) + .save(airplane); + verifyNoMoreInteractions(airplaneRepository); + assertThat(actualAirplane.getId()).isEqualTo(airplane.getId()); + assertThat(actualAirplane.getName()).isEqualTo(airplane.getName()); + assertThat(actualAirplane.getType()).isEqualTo(airplane.getType()); + assertThat(actualAirplane.getCapacity()).isEqualTo(airplane.getCapacity()); + } +} \ No newline at end of file diff --git a/core/src/test/java/cz/muni/fi/pa165/core/service/airplanetype/AirplaneTypeServiceTest.java b/core/src/test/java/cz/muni/fi/pa165/core/service/airplanetype/AirplaneTypeServiceTest.java new file mode 100644 index 0000000000000000000000000000000000000000..0ab856bf2da7e95d14ee2bfbba75737156716b6f --- /dev/null +++ b/core/src/test/java/cz/muni/fi/pa165/core/service/airplanetype/AirplaneTypeServiceTest.java @@ -0,0 +1,61 @@ +package cz.muni.fi.pa165.core.service.airplanetype; + +import cz.muni.fi.pa165.core.data.domain.AirplaneType; +import cz.muni.fi.pa165.core.data.repository.airplanetype.AirplaneTypeRepository; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; + +import java.util.Optional; + +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.*; +import static org.assertj.core.api.Assertions.assertThat; + +@SpringBootTest +class AirplaneTypeServiceTest { + + @Autowired + private AirplaneTypeService airplaneTypeService; + + @MockBean + private AirplaneTypeRepository airplaneTypeRepository; + + private static final AirplaneType airplaneType = new AirplaneType(); + + @BeforeAll + static void beforeAll() { + airplaneType.setId(42L); + airplaneType.setName("A320"); + } + + @Test + void findByNameWhenPresent() { + when(airplaneTypeRepository.findByName(anyString())) + .thenReturn(Optional.of(airplaneType)); + + Optional<AirplaneType> actualAirplaneType = airplaneTypeService.findByName("A320"); + + verify(airplaneTypeRepository, times(1)) + .findByName("A320"); + verifyNoMoreInteractions(airplaneTypeRepository); + assertThat(actualAirplaneType.isPresent()).isTrue(); + assertThat(actualAirplaneType.get().getId()).isEqualTo(airplaneType.getId()); + assertThat(actualAirplaneType.get().getName()).isEqualTo(airplaneType.getName()); + } + + @Test + void findByNameWhenNotPresent() { + when(airplaneTypeRepository.findByName(anyString())) + .thenReturn(Optional.empty()); + + Optional<AirplaneType> actualAirplaneType = airplaneTypeService.findByName("A320"); + + verify(airplaneTypeRepository, times(1)) + .findByName("A320"); + verifyNoMoreInteractions(airplaneTypeRepository); + assertThat(actualAirplaneType.isEmpty()).isTrue(); + } +} \ No newline at end of file diff --git a/core/src/test/java/cz/muni/fi/pa165/core/service/airport/AirportServiceImplTest.java b/core/src/test/java/cz/muni/fi/pa165/core/service/airport/AirportServiceImplTest.java new file mode 100644 index 0000000000000000000000000000000000000000..7ee594ee3016456b38b4b4a2265a225f0ff8a0c1 --- /dev/null +++ b/core/src/test/java/cz/muni/fi/pa165/core/service/airport/AirportServiceImplTest.java @@ -0,0 +1,122 @@ +package cz.muni.fi.pa165.core.service.airport; + +import cz.muni.fi.pa165.core.data.domain.Airport; +import cz.muni.fi.pa165.core.data.domain.City; +import cz.muni.fi.pa165.core.data.repository.airport.AirportRepository; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +class AirportServiceImplTest { + + private AirportServiceImpl airportService; + private AirportRepository airportRepository; + + @BeforeEach + void setUp() { + airportRepository = mock(AirportRepository.class); + airportService = new AirportServiceImpl(airportRepository); + } + + @Test + void findByNameTestSuccess() { + var airportName = "Paris Charles de Gaulle"; + var airport = new Airport(); + airport.setName(airportName); + + when(airportRepository.findByName(airportName)) + .thenReturn(Optional.of(airport)); + + var foundCityByNameOpt = airportService.findByName(airportName); + + assertTrue(foundCityByNameOpt.isPresent()); + assertEquals(airport.getName(), foundCityByNameOpt.get().getName()); + assertEquals(airport, foundCityByNameOpt.get()); + } + + @Test + void findByCodeTestSuccess() { + var airportCode = "JFK"; + var airport = new Airport(); + airport.setCode(airportCode); + + when(airportRepository.findByCode(airportCode)) + .thenReturn(Optional.of(airport)); + + var foundCityByCodeOpt = airportService.findByCode(airportCode); + + assertTrue(foundCityByCodeOpt.isPresent()); + assertEquals(airport.getName(), foundCityByCodeOpt.get().getName()); + assertEquals(airport, foundCityByCodeOpt.get()); + } + + @Test + void findByCityTestSuccess() { + var city = new City(); + city.setName("Prague"); + + var airport = new Airport(); + airport.setCity(city); + + when(airportRepository.findByCity(city)) + .thenReturn(List.of(airport)); + + var foundAirportsByCity = airportService.findByCity(city); + + assertFalse(foundAirportsByCity.isEmpty()); + assertEquals(airport.getName(), foundAirportsByCity.get(0).getName()); + assertEquals(airport, foundAirportsByCity.get(0)); + } + + @Test + void findByCityTestFindsNoAirports() { + var city = new City(); + city.setName("Namestovo"); + + when(airportRepository.findByCity(city)) + .thenReturn(new ArrayList<>()); + + var foundAirportsByCity = airportService.findByCity(city); + + assertTrue(foundAirportsByCity.isEmpty()); + } + + @Test + void updateTestSuccess() { + var airportToUpdate = new Airport(); + airportToUpdate.setId(1L); + var heathrow = "Heathrow"; + airportToUpdate.setName(heathrow); + airportToUpdate.setCity(new City()); + airportToUpdate.getCity().setName("London"); + var lhr = "LHR"; + airportToUpdate.setCode(lhr); + + var updatedAirportRequest = new Airport(); + var stansted = "Stansted"; + updatedAirportRequest.setName(stansted); + var stn = "STN"; + updatedAirportRequest.setCode(stn); + + when(airportRepository.findById(1L)) + .thenReturn(Optional.of(airportToUpdate)); + when(airportRepository.save(airportToUpdate)) + .thenReturn(updatedAirportRequest); + + var updatedCountry = airportService.update(1L, updatedAirportRequest); + + assertEquals(airportToUpdate.getName(), updatedCountry.getName()); + assertEquals(airportToUpdate, updatedCountry); + assertEquals(stansted, airportToUpdate.getName()); + assertNotEquals(heathrow, airportToUpdate.getName()); + assertEquals(stn, airportToUpdate.getCode()); + assertNotEquals(lhr, airportToUpdate.getCode()); + } +} \ No newline at end of file diff --git a/core/src/test/java/cz/muni/fi/pa165/core/service/city/CityServiceImplTest.java b/core/src/test/java/cz/muni/fi/pa165/core/service/city/CityServiceImplTest.java new file mode 100644 index 0000000000000000000000000000000000000000..25d79c66bcbd1351adcc98b4422d3f644514ed35 --- /dev/null +++ b/core/src/test/java/cz/muni/fi/pa165/core/service/city/CityServiceImplTest.java @@ -0,0 +1,90 @@ +package cz.muni.fi.pa165.core.service.city; + +import cz.muni.fi.pa165.core.data.domain.City; +import cz.muni.fi.pa165.core.data.repository.city.CityRepository; +import cz.muni.fi.pa165.core.exceptions.ResourceNotFoundException; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +class CityServiceImplTest { + + private CityServiceImpl cityService; + private CityRepository cityRepository; + + @BeforeEach + void setUp() { + cityRepository = mock(CityRepository.class); + cityService = new CityServiceImpl(cityRepository); + } + + @Test + void findByNameTestSuccess() { + var cityName = "Berlin"; + var city = new City(); + city.setId(1L); + city.setName(cityName); + + when(cityRepository.findByName(cityName)) + .thenReturn(Optional.of(city)); + + var foundCityByNameOpt = cityService.findByName(cityName); + + assertTrue(foundCityByNameOpt.isPresent()); + assertEquals(city.getName(), foundCityByNameOpt.get().getName()); + assertEquals(city, foundCityByNameOpt.get()); + } + + @Test + void findByNameTestFindsEmpty() { + var cityName = "Prague"; + + when(cityRepository.findByName(cityName)) + .thenReturn(Optional.empty()); + + var foundCityByNameOpt = cityService.findByName(cityName); + + assertTrue(foundCityByNameOpt.isEmpty()); + } + + @Test + void updateTestNotFoundByIdThrowsException() { + var city = new City(); + city.setId(-1L); + city.setName("Madrid"); + + when(cityService.findById(-1L)) + .thenReturn(Optional.empty()); + + assertThrows(ResourceNotFoundException.class, () -> cityService.update(-1L, city)); + } + + @Test + void updateTestSuccess() { + var cityToUpdate = new City(); + cityToUpdate.setId(1L); + var amsterdam = "Amsterdam"; + cityToUpdate.setName(amsterdam); + + var updatedCityRequest = new City(); + var antwerp = "Antwerp"; + updatedCityRequest.setName(antwerp); + + when(cityRepository.findById(1L)) + .thenReturn(Optional.of(cityToUpdate)); + when(cityRepository.save(cityToUpdate)) + .thenReturn(updatedCityRequest); + + var updatedCountry = cityService.update(1L, updatedCityRequest); + + assertEquals(cityToUpdate.getName(), updatedCountry.getName()); + assertEquals(cityToUpdate, updatedCountry); + assertEquals(antwerp, cityToUpdate.getName()); + assertNotEquals(amsterdam, cityToUpdate.getName()); + } +} \ No newline at end of file diff --git a/core/src/test/java/cz/muni/fi/pa165/core/service/common/BaseServiceTest.java b/core/src/test/java/cz/muni/fi/pa165/core/service/common/BaseServiceTest.java new file mode 100644 index 0000000000000000000000000000000000000000..b37ae53212ac802fb32a3ba69c109dc0dea79af0 --- /dev/null +++ b/core/src/test/java/cz/muni/fi/pa165/core/service/common/BaseServiceTest.java @@ -0,0 +1,95 @@ +package cz.muni.fi.pa165.core.service.common; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +/** + * In this test we use {@code BaseServiceImpl<TestObject,Long>}. + * However, for the sake of not writing everywhere {@code <TestObject,Long>}, + * we created the aliases {@link TestObjectRepository}, {@link TestObjectService} + * and {@link TestObjectServiceImpl}. + */ +@SpringBootTest +class BaseServiceTest { + + @Autowired + private TestObjectService testObjectService; + + @MockBean + private TestObjectRepository testObjectRepository; + + private static final TestObject sampleTestObject = new TestObject("description"); + private static final TestObject sampleTestObject1 = new TestObject("desc1"); + private static final TestObject sampleTestObject2 = new TestObject("desc2"); + private static final List<TestObject> allSampleTestObjects = new ArrayList<>(); + + @BeforeAll + static void beforeAll() { + sampleTestObject.setId(42L); + sampleTestObject1.setId(1L); + sampleTestObject2.setId(2L); + allSampleTestObjects.addAll(List.of(sampleTestObject1, sampleTestObject2)); + } + + @Test + void save() { + when(testObjectRepository.save(any(TestObject.class))) + .thenReturn(sampleTestObject); + TestObject actualObject = testObjectService.save(new TestObject("")); + assertThat(actualObject).isEqualTo(sampleTestObject); + } + + @Test + void findById() { + when(testObjectRepository.findById(42L)) + .thenReturn(Optional.of(sampleTestObject)); + Optional<TestObject> actualObject = testObjectService.findById(42L); + assertThat(actualObject.isPresent()).isTrue(); + assertThat(actualObject.get()).isEqualTo(sampleTestObject); + } + + @Test + void findAll() { + when(testObjectRepository.findAll()) + .thenReturn(allSampleTestObjects); + List<TestObject> actualObjects = testObjectService.findAll(); + assertThat(actualObjects.size()).isEqualTo(2); + assertThat(actualObjects.get(0)).isEqualTo(sampleTestObject1); + assertThat(actualObjects.get(1)).isEqualTo(sampleTestObject2); + } + + @Test + void deleteById() { + testObjectService.deleteById(2L); + + verify(testObjectRepository, times(1)).deleteById(2L); + verifyNoMoreInteractions(testObjectRepository); + } + + @Test + void deleteAll() { + testObjectService.deleteAll(); + + verify(testObjectRepository, times(1)).deleteAll(); + verifyNoMoreInteractions(testObjectRepository); + } + + @Test + void existsById() { + testObjectService.existsById(1L); + + verify(testObjectRepository, times(1)).existsById(1L); + verifyNoMoreInteractions(testObjectRepository); + } +} diff --git a/core/src/test/java/cz/muni/fi/pa165/core/service/common/TestObject.java b/core/src/test/java/cz/muni/fi/pa165/core/service/common/TestObject.java new file mode 100644 index 0000000000000000000000000000000000000000..4f27f445b33b8b76c377d06fefc14eb2011abe34 --- /dev/null +++ b/core/src/test/java/cz/muni/fi/pa165/core/service/common/TestObject.java @@ -0,0 +1,26 @@ +package cz.muni.fi.pa165.core.service.common; + +import cz.muni.fi.pa165.core.data.domain.common.DomainEntity; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; + +/** + * Sample object representing entity for the purpose of tests. + */ +@Entity +public class TestObject extends DomainEntity { + + @Column + private String description; + + public TestObject() { + } + + public TestObject(String description) { + this.description = description; + } + + public String getDescription() { + return description; + } +} diff --git a/core/src/test/java/cz/muni/fi/pa165/core/service/common/TestObjectRepository.java b/core/src/test/java/cz/muni/fi/pa165/core/service/common/TestObjectRepository.java new file mode 100644 index 0000000000000000000000000000000000000000..958a594a9ec390645256c7b4655db9d9132b592e --- /dev/null +++ b/core/src/test/java/cz/muni/fi/pa165/core/service/common/TestObjectRepository.java @@ -0,0 +1,9 @@ +package cz.muni.fi.pa165.core.service.common; + +import cz.muni.fi.pa165.core.data.repository.common.BaseRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface TestObjectRepository extends BaseRepository<TestObject, Long> { + +} diff --git a/core/src/test/java/cz/muni/fi/pa165/core/service/common/TestObjectService.java b/core/src/test/java/cz/muni/fi/pa165/core/service/common/TestObjectService.java new file mode 100644 index 0000000000000000000000000000000000000000..8b8bd69ca8b54c5d8d9bb4268bf57faac18483dc --- /dev/null +++ b/core/src/test/java/cz/muni/fi/pa165/core/service/common/TestObjectService.java @@ -0,0 +1,8 @@ +package cz.muni.fi.pa165.core.service.common; + +import org.springframework.stereotype.Service; + +@Service +public interface TestObjectService extends BaseService<TestObject, Long> { + +} diff --git a/core/src/test/java/cz/muni/fi/pa165/core/service/common/TestObjectServiceImpl.java b/core/src/test/java/cz/muni/fi/pa165/core/service/common/TestObjectServiceImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..e6abe97108cbcc3b14e48d0aa8663fa28f5c3cde --- /dev/null +++ b/core/src/test/java/cz/muni/fi/pa165/core/service/common/TestObjectServiceImpl.java @@ -0,0 +1,17 @@ +package cz.muni.fi.pa165.core.service.common; + +import cz.muni.fi.pa165.core.data.repository.common.BaseRepository; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +@Service +public class TestObjectServiceImpl extends BaseServiceImpl<TestObject, Long> implements TestObjectService{ + + private final TestObjectRepository testObjectRepository; + + @Autowired + public TestObjectServiceImpl(TestObjectRepository testObjectRepository) { + super(testObjectRepository); + this.testObjectRepository = testObjectRepository; + } +} diff --git a/core/src/test/java/cz/muni/fi/pa165/core/service/country/CountryServiceImplTest.java b/core/src/test/java/cz/muni/fi/pa165/core/service/country/CountryServiceImplTest.java new file mode 100644 index 0000000000000000000000000000000000000000..83a045171eddfd1003606399f381c7c02841dbfd --- /dev/null +++ b/core/src/test/java/cz/muni/fi/pa165/core/service/country/CountryServiceImplTest.java @@ -0,0 +1,74 @@ +package cz.muni.fi.pa165.core.service.country; + +import cz.muni.fi.pa165.core.data.domain.Country; +import cz.muni.fi.pa165.core.data.repository.country.CountryRepository; +import cz.muni.fi.pa165.core.exceptions.ResourceNotFoundException; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +class CountryServiceImplTest { + + private CountryServiceImpl countryService; + private CountryRepository countryRepository; + + @BeforeEach + void setUp() { + countryRepository = mock(CountryRepository.class); + countryService = new CountryServiceImpl(countryRepository); + } + + @Test + void findByNameTestSuccess() { + var countryName = "Italy"; + var country = new Country(); + country.setId(1L); + country.setName(countryName); + + when(countryRepository.findByName(countryName)) + .thenReturn(Optional.of(country)); + + var foundCountryByNameOpt = countryService.findByName(countryName); + + assertTrue(foundCountryByNameOpt.isPresent()); + assertEquals(country.getName(), foundCountryByNameOpt.get().getName()); + assertEquals(country, foundCountryByNameOpt.get()); + } + + @Test + void updateTestNotFoundByIdThrowsException() { + var country = new Country(); + country.setId(-1L); + country.setName("France"); + + when(countryRepository.findById(-1L)) + .thenReturn(Optional.empty()); + + assertThrows(ResourceNotFoundException.class, () -> countryService.update(-1L, country)); + } + + @Test + void updateTestSuccess() { + var countryToUpdate = new Country(); + countryToUpdate.setId(1L); + countryToUpdate.setName("France"); + + var updatedCountryRequest = new Country(); + updatedCountryRequest.setName("Switzerland"); + + when(countryRepository.findById(1L)) + .thenReturn(Optional.of(countryToUpdate)); + when(countryRepository.save(countryToUpdate)) + .thenReturn(updatedCountryRequest); + + var updatedCountry = countryService.update(1L, updatedCountryRequest); + + assertEquals(countryToUpdate.getName(), updatedCountry.getName()); + assertEquals(countryToUpdate, updatedCountry); + } +} diff --git a/core/src/test/java/cz/muni/fi/pa165/core/service/flight/FlightServiceImplTests.java b/core/src/test/java/cz/muni/fi/pa165/core/service/flight/FlightServiceImplTests.java new file mode 100644 index 0000000000000000000000000000000000000000..264273639ef4ac8cb99e4ef183ae6cbb71280684 --- /dev/null +++ b/core/src/test/java/cz/muni/fi/pa165/core/service/flight/FlightServiceImplTests.java @@ -0,0 +1,95 @@ +package cz.muni.fi.pa165.core.service.flight; + +import cz.muni.fi.pa165.core.data.domain.Flight; +import cz.muni.fi.pa165.core.data.repository.flight.FlightRepository; +import cz.muni.fi.pa165.core.service.flight.FlightServiceImpl; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.time.OffsetDateTime; +import java.time.ZoneOffset; +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + + +public class FlightServiceImplTests { + private FlightServiceImpl flightService; + private FlightRepository flightRepository; + + + @BeforeEach + void setUp() { + flightRepository = mock(FlightRepository.class); + flightService = new FlightServiceImpl(flightRepository); + } + + @Test + void findByIdTestSuccess() { + var flightDepartureTime = OffsetDateTime.now(); + var flightArrivalTime = OffsetDateTime.now(); + var flight = new Flight(); + flight.setArrivalTime(flightArrivalTime); + flight.setDepartureTime(flightDepartureTime); + + when(flightRepository.findById(flight.getId())) + .thenReturn(Optional.of(flight)); + + var foundFlightByIdOpt = flightService.findById(flight.getId()); + + assertTrue(foundFlightByIdOpt.isPresent()); + assertEquals(flight.getArrivalTime(), foundFlightByIdOpt.get().getArrivalTime()); + assertEquals(flight.getDepartureTime(), foundFlightByIdOpt.get().getDepartureTime()); + } + + @Test + void findWithStewardsTestSuccess() { + var flightDepartureTime = OffsetDateTime.now(); + var flightArrivalTime = OffsetDateTime.now(); + var flight = new Flight(); + flight.setArrivalTime(flightArrivalTime); + flight.setDepartureTime(flightDepartureTime); + + when(flightRepository.findByIdWithStewards(flight.getId())) + .thenReturn(Optional.of(flight)); + + var foundFlightByIdOpt = flightService.findByIdWithStewards(flight.getId()); + + assertTrue(foundFlightByIdOpt.isPresent()); + assertEquals(flight.getFlightStewards(), foundFlightByIdOpt.get().getFlightStewards()); + } + + @Test + void updateTestSuccess() { + var flightToUpdate = new Flight(); + flightToUpdate.setId(1L); + var time = OffsetDateTime.of(2000, 4, 9, 20, 15, 45, 345875000, ZoneOffset.of("+07:00")); + flightToUpdate.setDepartureTime(time); + flightToUpdate.setArrivalTime(time); + + + var updatedFlightRequest = new Flight(); + var newTime = OffsetDateTime.of(1980, 4, 9, 20, 15, 45, 345875000, ZoneOffset.of("+07:00")); + updatedFlightRequest.setDepartureTime(newTime); + updatedFlightRequest.setArrivalTime(newTime); + + when(flightRepository.findById(1L)) + .thenReturn(Optional.of(flightToUpdate)); + when(flightRepository.save(flightToUpdate)) + .thenReturn(updatedFlightRequest); + + + assertEquals(time, flightToUpdate.getDepartureTime()); + assertNotEquals(newTime, flightToUpdate.getDepartureTime()); + assertEquals(time, flightToUpdate.getArrivalTime()); + assertNotEquals(newTime, flightToUpdate.getArrivalTime()); + + var afterUpdate = flightService.update(1L, updatedFlightRequest); + + assertEquals(afterUpdate.getDepartureTime(), flightToUpdate.getDepartureTime()); + + } + +} diff --git a/core/src/test/java/cz/muni/fi/pa165/core/service/steward/StewardServiceImplTests.java b/core/src/test/java/cz/muni/fi/pa165/core/service/steward/StewardServiceImplTests.java new file mode 100644 index 0000000000000000000000000000000000000000..44575b2f0dd31de53021d54fe07c802dce0e0755 --- /dev/null +++ b/core/src/test/java/cz/muni/fi/pa165/core/service/steward/StewardServiceImplTests.java @@ -0,0 +1,105 @@ +package cz.muni.fi.pa165.core.service.steward; + +import cz.muni.fi.pa165.core.data.domain.Flight; +import cz.muni.fi.pa165.core.data.domain.FlightSteward; +import cz.muni.fi.pa165.core.data.domain.Steward; +import cz.muni.fi.pa165.core.data.repository.flightsteward.FlightStewardRepository; +import cz.muni.fi.pa165.core.data.repository.steward.StewardRepository; +import cz.muni.fi.pa165.core.service.steward.StewardServiceImpl; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import java.time.OffsetDateTime; +import java.util.Optional; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + + +public class StewardServiceImplTests { + private StewardServiceImpl stewardService; + private StewardRepository stewardRepository; + + @BeforeEach + void setUp() { + stewardRepository = mock(StewardRepository.class); + FlightStewardRepository flightStewardRepository = mock(FlightStewardRepository.class); + stewardService = new StewardServiceImpl(stewardRepository, + flightStewardRepository); + } + + @Test + void findByIdTestSuccess() { + var firstName = "Ondra"; + var lastName = "Nový"; + var steward = new Steward(); + steward.setFirstName(firstName); + steward.setLastName(lastName); + + when(stewardRepository.findById(steward.getId())) + .thenReturn(Optional.of(steward)); + + var foundStewardByIdOpt = stewardService.findById(steward.getId()); + + assertTrue(foundStewardByIdOpt.isPresent()); + assertEquals(foundStewardByIdOpt.get().getFirstName(), firstName); + assertEquals(foundStewardByIdOpt.get().getLastName(), lastName); + } + + @Test + void assignToFlight() { + var firstName = "Ondra"; + var lastName = "Nový"; + var steward = new Steward(); + steward.setFirstName(firstName); + steward.setLastName(lastName); + + var flightDepartureTime = OffsetDateTime.now(); + var flightArrivalTime = OffsetDateTime.now(); + var flight = new Flight(); + flight.setArrivalTime(flightArrivalTime); + flight.setDepartureTime(flightDepartureTime); + + FlightSteward flightSteward = new FlightSteward(); + + flightSteward.setFlight(flight); + flightSteward.setSteward(steward); + + assertEquals(flightSteward.getSteward(), steward); + assertEquals(flightSteward.getFlight(), flight); + } + + + @Test + void updateTestSuccess() { + var stewardToUpdate = new Steward(); + stewardToUpdate.setId(1L); + var firstName = "Ondra"; + var lastName = "Nový"; + stewardToUpdate.setFirstName(firstName); + stewardToUpdate.setLastName(lastName); + + + var updatedStewardRequest = new Steward(); + var newFirstName = "Jirka"; + var newLastName = "Starý"; + updatedStewardRequest.setFirstName(newFirstName); + updatedStewardRequest.setLastName(newLastName); + + when(stewardRepository.findById(1L)) + .thenReturn(Optional.of(stewardToUpdate)); + when(stewardRepository.save(stewardToUpdate)) + .thenReturn(updatedStewardRequest); + + + assertEquals(firstName, stewardToUpdate.getFirstName()); + assertNotEquals(newFirstName, stewardToUpdate.getFirstName()); + assertEquals(lastName, stewardToUpdate.getLastName()); + assertNotEquals(newLastName, stewardToUpdate.getLastName()); + + var afterUpdate = stewardService.update(1L, updatedStewardRequest); + + assertEquals(afterUpdate.getFirstName(), stewardToUpdate.getFirstName()); + } + +} + diff --git a/dashboards/dashboard.yaml b/dashboards/dashboard.yaml new file mode 100644 index 0000000000000000000000000000000000000000..b5df708530ea53752bf1bf1b926acc851d3d83b9 --- /dev/null +++ b/dashboards/dashboard.yaml @@ -0,0 +1,7 @@ +apiVersion: 1 + +providers: + - name: 'Default' + folder: 'Services' + options: + path: /etc/grafana/provisioning/dashboards diff --git a/dashboards/grafana.json b/dashboards/grafana.json new file mode 100644 index 0000000000000000000000000000000000000000..c035a36562fb1b1c2185b4bab5f66d7f6a53c35b --- /dev/null +++ b/dashboards/grafana.json @@ -0,0 +1,109 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "target": { + "limit": 100, + "matchAny": false, + "tags": [], + "type": "dashboard" + }, + "type": "dashboard" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "id": 1, + "links": [], + "liveNow": false, + "panels": [ + { + "datasource": {}, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 9 + }, + "id": 8, + "options": { + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showThresholdLabels": false, + "showThresholdMarkers": true + }, + "pluginVersion": "9.1.7", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "p5SIZnEVk" + }, + "editorMode": "code", + "expr": "http_server_requests_seconds_count", + "legendFormat": "__auto", + "range": true, + "refId": "A" + } + ], + "title": "HTTP Server Requests counter", + "type": "gauge" + } + ], + "refresh": "5s", + "schemaVersion": 37, + "style": "dark", + "tags": [], + "templating": { + "list": [] + }, + "time": { + "from": "now-6h", + "to": "now" + }, + "timepicker": {}, + "timezone": "", + "title": "Airport-Manager", + "uid": "YDzAhnEVz", + "version": 3, + "weekStart": "" +} diff --git a/datasource.yml b/datasource.yml new file mode 100644 index 0000000000000000000000000000000000000000..d96153aa026474ed8aa7882e1586d0334623d4e1 --- /dev/null +++ b/datasource.yml @@ -0,0 +1,7 @@ +apiVersion: 1 + +datasources: + - name: Prometheus + type: prometheus + access: proxy + url: http://prometheus:9090 diff --git a/locust-scenarios/locustfile.py b/locust-scenarios/locustfile.py new file mode 100644 index 0000000000000000000000000000000000000000..4c9665f114e8bf1ed6487af92c137e2774e4df79 --- /dev/null +++ b/locust-scenarios/locustfile.py @@ -0,0 +1,139 @@ +from locust import HttpUser, task + + +class Admin(HttpUser): + auth_header = {'Authorization': 'Bearer eyJraWQiOiJyc2ExIiwidHlwIjoiYXQrand0IiwiYWxnIjoiUlMyNTYifQ.eyJhdWQiOiI3ZTAyYTBhOS00NDZhLTQxMmQtYWQyYi05MGFkZDQ3YjBmZGQiLCJzdWIiOiI0OTMyMzdAbXVuaS5jeiIsImFjciI6Imh0dHBzOi8vcmVmZWRzLm9yZy9wcm9maWxlL3NmYSIsInNjb3BlIjoidGVzdF80IHRlc3RfMyB0ZXN0XzIgdGVzdF8xIHRlc3RfcmVhZCBvcGVuaWQgcHJvZmlsZSBlZHVwZXJzb25fc2NvcGVkX2FmZmlsaWF0aW9uIHRlc3Rfd3JpdGUgZW1haWwgdGVzdF81IiwiYXV0aF90aW1lIjoxNjgzNDk1NDgwLCJpc3MiOiJodHRwczovL29pZGMubXVuaS5jei9vaWRjLyIsImV4cCI6MTY4MzQ5OTEzOSwiaWF0IjoxNjgzNDk1NTM5LCJjbGllbnRfaWQiOiI3ZTAyYTBhOS00NDZhLTQxMmQtYWQyYi05MGFkZDQ3YjBmZGQiLCJqdGkiOiIzMjgxZmJiYy01MWYxLTQ0ZGEtOGMzNi1hY2Q5OTM1N2QxYzUifQ.BVLruvROGTvJh6CmhtjKIhURSheMCOVPADKgI6P-qhTmbhi_jTT4gvnLHsAwImGehmwDvo2seLAGahRdv84iM3iYJ7WTblPTGhu9CI5SAE-59ROlGtUaA-3q8zL2xdOVtF5in_KjF8lrk2gI8uY4i3iRE-PZnm9EyK4qmgzcCXIiu9K3TAPnDtKPs8sileFj7_V6Xq4qmjq7g1dr_jGOcelesUgnq6fQlDLvb2guUwWHFTkWZQSnS-cEAX6ZkROkuzPi7d53YlQaPSVngjMYpd81y_DypDcUqbrzNYK28y5cBLbLZ2y2CWKqKyE4ENryc237AH_UcaVdFWwf6Vh2gQ'} + def on_start(self): + + #create airplane type - just once because creating airplane type more times would report fail due to not unique name + self.client.post(":8080/api/airplaneTypes", json= + { + "name": "PráškovaÄ 2000" + }, + headers = self.auth_header) + + response = self.client.get(":8080/api/airplaneTypes/1") + + if response.status_code == 200: + self.client.post(":8080/api/airplanes", json= + { + "name": "Prášek", + "capacity": 2, + "typeId": 1 + }, + headers = self.auth_header) + + self.client.post(":8080/api/countries", json= + { + "name": "Slovensko" + }, + headers = self.auth_header) + self.client.post(":8080/api/cities", json= + { + "name": "HolÃÄ" + }, + headers = self.auth_header) + self.client.post(":8080/api/cities", json= + { + "name": "Senica" + }, + headers = self.auth_header) + self.client.post(":8080/api/cities/1/countries/1", json= + { + "name": "Senica" + }, + headers = self.auth_header) + self.client.post(":8080/api/cities/2/countries/1", json= + { + "name": "Senica" + }, + headers = self.auth_header) + #create 2 airports - just once because airport need to have unique code + self.client.post(":8080/api/airports", json= + { + "name": "Travnik Holic", + "code": "THL", + "location": { + "latitude": 41.40338, + "longitude": 2.17403 + } + }, + headers = self.auth_header) + self.client.post(":8080/api/airports", json= + { + "name": "Hliniste Senica", + "code": "HSL", + "location": { + "latitude": 41.40338, + "longitude": 2.17403 + } + }, + headers = self.auth_header) + + @task + def create_steward(self): + self.client.post(":8080/api/stewards", json= + { + "firstName": "John", + "lastName": "Doe" + }, + headers = self.auth_header) + + @task + def create_flight(self): + self.client.post(":8080/api/flights", json= + { + "departureTime": "2023-12-22T12:04:04.493908908+01:00", + "arrivalTime": "2023-12-22T12:04:04.493908908+01:00", + "airplaneId": 1 + }, + headers = self.auth_header) + + @task + def assign_steward_flight(self): + response_steward = self.client.get(":8080/api/stewards/1", headers = self.auth_header) + response_flight = self.client.get(":8080/api/flights/1", headers = self.auth_header) + + if response_steward.status_code == 200 and response_flight.status_code == 200: + self.client.post(":8080/api/stewards/1/flights/1", headers = self.auth_header) + self.client.delete(":8080/api/stewards/1/flights/1", headers = self.auth_header) + + @task + def assign_airport_flight(self): + response_airport1 = self.client.get(":8080/api/airports/1", headers = self.auth_header) + response_airport2 = self.client.get(":8080/api/airports/2", headers = self.auth_header) + response_flight = self.client.get(":8080/api/flights/1", headers = self.auth_header) + + if response_airport1.status_code == 200 and response_flight.status_code == 200 and response_airport2.status_code == 200: + self.client.post(":8080/api/airports/1/departingFlights/1", headers = self.auth_header) + self.client.post(":8080/api/airports/2/arrivingFlights/1", headers = self.auth_header) + self.client.delete(":8080/api/airports/1/departingFlights/1", headers = self.auth_header) + self.client.delete(":8080/api/airports/2/arrivingFlights/1", headers = self.auth_header) + + +class BasicUser(HttpUser): + min_wait = 5000 + max_wait = 15000 + auth_header = {'Authorization': 'Bearer eyJraWQiOiJyc2ExIiwidHlwIjoiYXQrand0IiwiYWxnIjoiUlMyNTYifQ.eyJhdWQiOiI3ZTAyYTBhOS00NDZhLTQxMmQtYWQyYi05MGFkZDQ3YjBmZGQiLCJzdWIiOiI0OTMyMzdAbXVuaS5jeiIsImFjciI6Imh0dHBzOi8vcmVmZWRzLm9yZy9wcm9maWxlL3NmYSIsInNjb3BlIjoidGVzdF80IHRlc3RfMyB0ZXN0XzIgdGVzdF8xIHRlc3RfcmVhZCBvcGVuaWQgcHJvZmlsZSBlZHVwZXJzb25fc2NvcGVkX2FmZmlsaWF0aW9uIHRlc3Rfd3JpdGUgZW1haWwgdGVzdF81IiwiYXV0aF90aW1lIjoxNjgzNDk1NDgwLCJpc3MiOiJodHRwczovL29pZGMubXVuaS5jei9vaWRjLyIsImV4cCI6MTY4MzQ5OTEzOSwiaWF0IjoxNjgzNDk1NTM5LCJjbGllbnRfaWQiOiI3ZTAyYTBhOS00NDZhLTQxMmQtYWQyYi05MGFkZDQ3YjBmZGQiLCJqdGkiOiIzMjgxZmJiYy01MWYxLTQ0ZGEtOGMzNi1hY2Q5OTM1N2QxYzUifQ.BVLruvROGTvJh6CmhtjKIhURSheMCOVPADKgI6P-qhTmbhi_jTT4gvnLHsAwImGehmwDvo2seLAGahRdv84iM3iYJ7WTblPTGhu9CI5SAE-59ROlGtUaA-3q8zL2xdOVtF5in_KjF8lrk2gI8uY4i3iRE-PZnm9EyK4qmgzcCXIiu9K3TAPnDtKPs8sileFj7_V6Xq4qmjq7g1dr_jGOcelesUgnq6fQlDLvb2guUwWHFTkWZQSnS-cEAX6ZkROkuzPi7d53YlQaPSVngjMYpd81y_DypDcUqbrzNYK28y5cBLbLZ2y2CWKqKyE4ENryc237AH_UcaVdFWwf6Vh2gQ'} + + @task + def get_airplane(self): + self.client.get(":8080/api/airplanes", headers = self.auth_header) + @task + def get_stewards(self): + self.client.get(":8080/api/stewards", headers = self.auth_header) + @task + def get_airports(self): + self.client.get(":8080/api/airports", headers = self.auth_header) + @task + def get_flights(self): + self.client.get(":8080/api/flights", headers = self.auth_header) + @task + def generate_airplane_report(self): + self.client.get(":8085/api/reports/airplane/1", headers = self.auth_header) + @task + def generate_airport_report(self): + self.client.get(":8085/api/reports/airport/1", headers = self.auth_header) + @task + def generate_flight_report(self): + self.client.get(":8085/api/reports/flight/1", headers = self.auth_header) diff --git a/mvnw b/mvnw new file mode 100755 index 0000000000000000000000000000000000000000..8d937f4c14f11f8c3aded556054af0b0097eaede --- /dev/null +++ b/mvnw @@ -0,0 +1,308 @@ +#!/bin/sh +# ---------------------------------------------------------------------------- +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# ---------------------------------------------------------------------------- + +# ---------------------------------------------------------------------------- +# Apache Maven Wrapper startup batch script, version 3.2.0 +# +# Required ENV vars: +# ------------------ +# JAVA_HOME - location of a JDK home dir +# +# Optional ENV vars +# ----------------- +# MAVEN_OPTS - parameters passed to the Java VM when running Maven +# e.g. to debug Maven itself, use +# set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +# MAVEN_SKIP_RC - flag to disable loading of mavenrc files +# ---------------------------------------------------------------------------- + +if [ -z "$MAVEN_SKIP_RC" ] ; then + + if [ -f /usr/local/etc/mavenrc ] ; then + . /usr/local/etc/mavenrc + fi + + if [ -f /etc/mavenrc ] ; then + . /etc/mavenrc + fi + + if [ -f "$HOME/.mavenrc" ] ; then + . "$HOME/.mavenrc" + fi + +fi + +# OS specific support. $var _must_ be set to either true or false. +cygwin=false; +darwin=false; +mingw=false +case "$(uname)" in + CYGWIN*) cygwin=true ;; + MINGW*) mingw=true;; + Darwin*) darwin=true + # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home + # See https://developer.apple.com/library/mac/qa/qa1170/_index.html + if [ -z "$JAVA_HOME" ]; then + if [ -x "/usr/libexec/java_home" ]; then + JAVA_HOME="$(/usr/libexec/java_home)"; export JAVA_HOME + else + JAVA_HOME="/Library/Java/Home"; export JAVA_HOME + fi + fi + ;; +esac + +if [ -z "$JAVA_HOME" ] ; then + if [ -r /etc/gentoo-release ] ; then + JAVA_HOME=$(java-config --jre-home) + fi +fi + +# For Cygwin, ensure paths are in UNIX format before anything is touched +if $cygwin ; then + [ -n "$JAVA_HOME" ] && + JAVA_HOME=$(cygpath --unix "$JAVA_HOME") + [ -n "$CLASSPATH" ] && + CLASSPATH=$(cygpath --path --unix "$CLASSPATH") +fi + +# For Mingw, ensure paths are in UNIX format before anything is touched +if $mingw ; then + [ -n "$JAVA_HOME" ] && [ -d "$JAVA_HOME" ] && + JAVA_HOME="$(cd "$JAVA_HOME" || (echo "cannot cd into $JAVA_HOME."; exit 1); pwd)" +fi + +if [ -z "$JAVA_HOME" ]; then + javaExecutable="$(which javac)" + if [ -n "$javaExecutable" ] && ! [ "$(expr "\"$javaExecutable\"" : '\([^ ]*\)')" = "no" ]; then + # readlink(1) is not available as standard on Solaris 10. + readLink=$(which readlink) + if [ ! "$(expr "$readLink" : '\([^ ]*\)')" = "no" ]; then + if $darwin ; then + javaHome="$(dirname "\"$javaExecutable\"")" + javaExecutable="$(cd "\"$javaHome\"" && pwd -P)/javac" + else + javaExecutable="$(readlink -f "\"$javaExecutable\"")" + fi + javaHome="$(dirname "\"$javaExecutable\"")" + javaHome=$(expr "$javaHome" : '\(.*\)/bin') + JAVA_HOME="$javaHome" + export JAVA_HOME + fi + fi +fi + +if [ -z "$JAVACMD" ] ; then + if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + else + JAVACMD="$(\unset -f command 2>/dev/null; \command -v java)" + fi +fi + +if [ ! -x "$JAVACMD" ] ; then + echo "Error: JAVA_HOME is not defined correctly." >&2 + echo " We cannot execute $JAVACMD" >&2 + exit 1 +fi + +if [ -z "$JAVA_HOME" ] ; then + echo "Warning: JAVA_HOME environment variable is not set." +fi + +# traverses directory structure from process work directory to filesystem root +# first directory with .mvn subdirectory is considered project base directory +find_maven_basedir() { + if [ -z "$1" ] + then + echo "Path not specified to find_maven_basedir" + return 1 + fi + + basedir="$1" + wdir="$1" + while [ "$wdir" != '/' ] ; do + if [ -d "$wdir"/.mvn ] ; then + basedir=$wdir + break + fi + # workaround for JBEAP-8937 (on Solaris 10/Sparc) + if [ -d "${wdir}" ]; then + wdir=$(cd "$wdir/.." || exit 1; pwd) + fi + # end of workaround + done + printf '%s' "$(cd "$basedir" || exit 1; pwd)" +} + +# concatenates all lines of a file +concat_lines() { + if [ -f "$1" ]; then + # Remove \r in case we run on Windows within Git Bash + # and check out the repository with auto CRLF management + # enabled. Otherwise, we may read lines that are delimited with + # \r\n and produce $'-Xarg\r' rather than -Xarg due to word + # splitting rules. + tr -s '\r\n' ' ' < "$1" + fi +} + +log() { + if [ "$MVNW_VERBOSE" = true ]; then + printf '%s\n' "$1" + fi +} + +BASE_DIR=$(find_maven_basedir "$(dirname "$0")") +if [ -z "$BASE_DIR" ]; then + exit 1; +fi + +MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"}; export MAVEN_PROJECTBASEDIR +log "$MAVEN_PROJECTBASEDIR" + +########################################################################################## +# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +# This allows using the maven wrapper in projects that prohibit checking in binary data. +########################################################################################## +wrapperJarPath="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" +if [ -r "$wrapperJarPath" ]; then + log "Found $wrapperJarPath" +else + log "Couldn't find $wrapperJarPath, downloading it ..." + + if [ -n "$MVNW_REPOURL" ]; then + wrapperUrl="$MVNW_REPOURL/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + else + wrapperUrl="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + fi + while IFS="=" read -r key value; do + # Remove '\r' from value to allow usage on windows as IFS does not consider '\r' as a separator ( considers space, tab, new line ('\n'), and custom '=' ) + safeValue=$(echo "$value" | tr -d '\r') + case "$key" in (wrapperUrl) wrapperUrl="$safeValue"; break ;; + esac + done < "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties" + log "Downloading from: $wrapperUrl" + + if $cygwin; then + wrapperJarPath=$(cygpath --path --windows "$wrapperJarPath") + fi + + if command -v wget > /dev/null; then + log "Found wget ... using wget" + [ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--quiet" + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + wget $QUIET "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" + else + wget $QUIET --http-user="$MVNW_USERNAME" --http-password="$MVNW_PASSWORD" "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" + fi + elif command -v curl > /dev/null; then + log "Found curl ... using curl" + [ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--silent" + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + curl $QUIET -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath" + else + curl $QUIET --user "$MVNW_USERNAME:$MVNW_PASSWORD" -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath" + fi + else + log "Falling back to using Java to download" + javaSource="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.java" + javaClass="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.class" + # For Cygwin, switch paths to Windows format before running javac + if $cygwin; then + javaSource=$(cygpath --path --windows "$javaSource") + javaClass=$(cygpath --path --windows "$javaClass") + fi + if [ -e "$javaSource" ]; then + if [ ! -e "$javaClass" ]; then + log " - Compiling MavenWrapperDownloader.java ..." + ("$JAVA_HOME/bin/javac" "$javaSource") + fi + if [ -e "$javaClass" ]; then + log " - Running MavenWrapperDownloader.java ..." + ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$wrapperUrl" "$wrapperJarPath") || rm -f "$wrapperJarPath" + fi + fi + fi +fi +########################################################################################## +# End of extension +########################################################################################## + +# If specified, validate the SHA-256 sum of the Maven wrapper jar file +wrapperSha256Sum="" +while IFS="=" read -r key value; do + case "$key" in (wrapperSha256Sum) wrapperSha256Sum=$value; break ;; + esac +done < "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties" +if [ -n "$wrapperSha256Sum" ]; then + wrapperSha256Result=false + if command -v sha256sum > /dev/null; then + if echo "$wrapperSha256Sum $wrapperJarPath" | sha256sum -c > /dev/null 2>&1; then + wrapperSha256Result=true + fi + elif command -v shasum > /dev/null; then + if echo "$wrapperSha256Sum $wrapperJarPath" | shasum -a 256 -c > /dev/null 2>&1; then + wrapperSha256Result=true + fi + else + echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." + echo "Please install either command, or disable validation by removing 'wrapperSha256Sum' from your maven-wrapper.properties." + exit 1 + fi + if [ $wrapperSha256Result = false ]; then + echo "Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised." >&2 + echo "Investigate or delete $wrapperJarPath to attempt a clean download." >&2 + echo "If you updated your Maven version, you need to update the specified wrapperSha256Sum property." >&2 + exit 1 + fi +fi + +MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" + +# For Cygwin, switch paths to Windows format before running java +if $cygwin; then + [ -n "$JAVA_HOME" ] && + JAVA_HOME=$(cygpath --path --windows "$JAVA_HOME") + [ -n "$CLASSPATH" ] && + CLASSPATH=$(cygpath --path --windows "$CLASSPATH") + [ -n "$MAVEN_PROJECTBASEDIR" ] && + MAVEN_PROJECTBASEDIR=$(cygpath --path --windows "$MAVEN_PROJECTBASEDIR") +fi + +# Provide a "standardized" way to retrieve the CLI args that will +# work with both Windows and non-Windows executions. +MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $*" +export MAVEN_CMD_LINE_ARGS + +WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +# shellcheck disable=SC2086 # safe args +exec "$JAVACMD" \ + $MAVEN_OPTS \ + $MAVEN_DEBUG_OPTS \ + -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ + "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ + ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" diff --git a/mvnw.cmd b/mvnw.cmd new file mode 100644 index 0000000000000000000000000000000000000000..c4586b564e6fa1bc22ae740b1308b258581f1c35 --- /dev/null +++ b/mvnw.cmd @@ -0,0 +1,205 @@ +@REM ---------------------------------------------------------------------------- +@REM Licensed to the Apache Software Foundation (ASF) under one +@REM or more contributor license agreements. See the NOTICE file +@REM distributed with this work for additional information +@REM regarding copyright ownership. The ASF licenses this file +@REM to you under the Apache License, Version 2.0 (the +@REM "License"); you may not use this file except in compliance +@REM with the License. You may obtain a copy of the License at +@REM +@REM http://www.apache.org/licenses/LICENSE-2.0 +@REM +@REM Unless required by applicable law or agreed to in writing, +@REM software distributed under the License is distributed on an +@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +@REM KIND, either express or implied. See the License for the +@REM specific language governing permissions and limitations +@REM under the License. +@REM ---------------------------------------------------------------------------- + +@REM ---------------------------------------------------------------------------- +@REM Apache Maven Wrapper startup batch script, version 3.2.0 +@REM +@REM Required ENV vars: +@REM JAVA_HOME - location of a JDK home dir +@REM +@REM Optional ENV vars +@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands +@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending +@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven +@REM e.g. to debug Maven itself, use +@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files +@REM ---------------------------------------------------------------------------- + +@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' +@echo off +@REM set title of command window +title %0 +@REM enable echoing by setting MAVEN_BATCH_ECHO to 'on' +@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% + +@REM set %HOME% to equivalent of $HOME +if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") + +@REM Execute a user defined script before this one +if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre +@REM check for pre script, once with legacy .bat ending and once with .cmd ending +if exist "%USERPROFILE%\mavenrc_pre.bat" call "%USERPROFILE%\mavenrc_pre.bat" %* +if exist "%USERPROFILE%\mavenrc_pre.cmd" call "%USERPROFILE%\mavenrc_pre.cmd" %* +:skipRcPre + +@setlocal + +set ERROR_CODE=0 + +@REM To isolate internal variables from possible post scripts, we use another setlocal +@setlocal + +@REM ==== START VALIDATION ==== +if not "%JAVA_HOME%" == "" goto OkJHome + +echo. +echo Error: JAVA_HOME not found in your environment. >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +:OkJHome +if exist "%JAVA_HOME%\bin\java.exe" goto init + +echo. +echo Error: JAVA_HOME is set to an invalid directory. >&2 +echo JAVA_HOME = "%JAVA_HOME%" >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +@REM ==== END VALIDATION ==== + +:init + +@REM Find the project base dir, i.e. the directory that contains the folder ".mvn". +@REM Fallback to current working directory if not found. + +set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% +IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir + +set EXEC_DIR=%CD% +set WDIR=%EXEC_DIR% +:findBaseDir +IF EXIST "%WDIR%"\.mvn goto baseDirFound +cd .. +IF "%WDIR%"=="%CD%" goto baseDirNotFound +set WDIR=%CD% +goto findBaseDir + +:baseDirFound +set MAVEN_PROJECTBASEDIR=%WDIR% +cd "%EXEC_DIR%" +goto endDetectBaseDir + +:baseDirNotFound +set MAVEN_PROJECTBASEDIR=%EXEC_DIR% +cd "%EXEC_DIR%" + +:endDetectBaseDir + +IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig + +@setlocal EnableExtensions EnableDelayedExpansion +for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a +@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% + +:endReadAdditionalConfig + +SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" +set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" +set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +set WRAPPER_URL="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + +FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( + IF "%%A"=="wrapperUrl" SET WRAPPER_URL=%%B +) + +@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +@REM This allows using the maven wrapper in projects that prohibit checking in binary data. +if exist %WRAPPER_JAR% ( + if "%MVNW_VERBOSE%" == "true" ( + echo Found %WRAPPER_JAR% + ) +) else ( + if not "%MVNW_REPOURL%" == "" ( + SET WRAPPER_URL="%MVNW_REPOURL%/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + ) + if "%MVNW_VERBOSE%" == "true" ( + echo Couldn't find %WRAPPER_JAR%, downloading it ... + echo Downloading from: %WRAPPER_URL% + ) + + powershell -Command "&{"^ + "$webclient = new-object System.Net.WebClient;"^ + "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^ + "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^ + "}"^ + "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%WRAPPER_URL%', '%WRAPPER_JAR%')"^ + "}" + if "%MVNW_VERBOSE%" == "true" ( + echo Finished downloading %WRAPPER_JAR% + ) +) +@REM End of extension + +@REM If specified, validate the SHA-256 sum of the Maven wrapper jar file +SET WRAPPER_SHA_256_SUM="" +FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( + IF "%%A"=="wrapperSha256Sum" SET WRAPPER_SHA_256_SUM=%%B +) +IF NOT %WRAPPER_SHA_256_SUM%=="" ( + powershell -Command "&{"^ + "$hash = (Get-FileHash \"%WRAPPER_JAR%\" -Algorithm SHA256).Hash.ToLower();"^ + "If('%WRAPPER_SHA_256_SUM%' -ne $hash){"^ + " Write-Output 'Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised.';"^ + " Write-Output 'Investigate or delete %WRAPPER_JAR% to attempt a clean download.';"^ + " Write-Output 'If you updated your Maven version, you need to update the specified wrapperSha256Sum property.';"^ + " exit 1;"^ + "}"^ + "}" + if ERRORLEVEL 1 goto error +) + +@REM Provide a "standardized" way to retrieve the CLI args that will +@REM work with both Windows and non-Windows executions. +set MAVEN_CMD_LINE_ARGS=%* + +%MAVEN_JAVA_EXE% ^ + %JVM_CONFIG_MAVEN_PROPS% ^ + %MAVEN_OPTS% ^ + %MAVEN_DEBUG_OPTS% ^ + -classpath %WRAPPER_JAR% ^ + "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" ^ + %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* +if ERRORLEVEL 1 goto error +goto end + +:error +set ERROR_CODE=1 + +:end +@endlocal & set ERROR_CODE=%ERROR_CODE% + +if not "%MAVEN_SKIP_RC%"=="" goto skipRcPost +@REM check for post script, once with legacy .bat ending and once with .cmd ending +if exist "%USERPROFILE%\mavenrc_post.bat" call "%USERPROFILE%\mavenrc_post.bat" +if exist "%USERPROFILE%\mavenrc_post.cmd" call "%USERPROFILE%\mavenrc_post.cmd" +:skipRcPost + +@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' +if "%MAVEN_BATCH_PAUSE%"=="on" pause + +if "%MAVEN_TERMINATE_CMD%"=="on" exit %ERROR_CODE% + +cmd /C exit /B %ERROR_CODE% diff --git a/pom.xml b/pom.xml index 23299b4757d0ae1324a999ddc78c392bc514436e..903a024b151357f7411a228b4db8b6083d8729a3 100644 --- a/pom.xml +++ b/pom.xml @@ -7,11 +7,11 @@ <version>1.0-SNAPSHOT</version> <modules> <module>core</module> - <module>authorization</module> - <module>authorization-client</module> + <module>user</module> <module>report</module> <module>weather</module> <module>core-client</module> + <module>user-client</module> <module>report-client</module> <module>weather-client</module> </modules> @@ -29,6 +29,18 @@ <id>540485</id> <name>Martin Slovik</name> </developer> + <developer> + <id>492778</id> + <name>Matej Hrica</name> + </developer> + <developer> + <id>492892</id> + <name>Adam KrÃdl</name> + </developer> + <developer> + <id>493237</id> + <name>Ján MacháÄek</name> + </developer> </developers> <parent> <groupId>org.springframework.boot</groupId> @@ -38,6 +50,8 @@ <!-- lookup parent from repository --> </parent> <properties> + <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> + <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> <maven.compiler.source>17</maven.compiler.source> <maven.compiler.target>17</maven.compiler.target> <lombok.version>1.18.26</lombok.version> @@ -61,7 +75,7 @@ </dependency> <dependency> <groupId>cz.muni.fi.pa165</groupId> - <artifactId>authorization</artifactId> + <artifactId>user</artifactId> <version>${version}</version> </dependency> <dependency> @@ -79,6 +93,11 @@ <artifactId>mapstruct</artifactId> <version>${org.mapstruct.version}</version> </dependency> + <dependency> + <groupId>org.modelmapper</groupId> + <artifactId>modelmapper</artifactId> + <version>3.0.0</version> + </dependency> <dependency> <groupId>org.springdoc</groupId> <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId> @@ -104,11 +123,6 @@ <artifactId>swagger-annotations</artifactId> <version>1.6.9</version> </dependency> - <dependency> - <groupId>javax.validation</groupId> - <artifactId>validation-api</artifactId> - <version>2.0.1.Final</version> - </dependency> <dependency> <groupId>javax.annotation</groupId> <artifactId>javax.annotation-api</artifactId> @@ -124,6 +138,11 @@ <artifactId>swagger-annotations-jakarta</artifactId> <version>${swagger-jakarta-version}</version> </dependency> + <dependency> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-starter-oauth2-resource-server</artifactId> + <version>3.0.4</version> + </dependency> </dependencies> </dependencyManagement> <build> @@ -176,7 +195,7 @@ <plugin> <groupId>org.openapitools</groupId> <artifactId>openapi-generator-maven-plugin</artifactId> - <version>6.4.0</version> + <version>6.5.0</version> </plugin> <plugin> <groupId>org.springdoc</groupId> diff --git a/prometheus.yml b/prometheus.yml new file mode 100644 index 0000000000000000000000000000000000000000..fadedee9716b5b5de22dbd23a934a34becb77993 --- /dev/null +++ b/prometheus.yml @@ -0,0 +1,16 @@ +global: + scrape_interval: 1s + external_labels: + monitor: 'my-monitor' + +scrape_configs: + - job_name: 'prometheus' + static_configs: + - targets: ['localhost:9090'] + - job_name: 'microservices' + metrics_path: /actuator/prometheus + static_configs: + - targets: ['localhost:8080'] + - targets: ['localhost:8083'] + - targets: ['localhost:8085'] + - targets: ['localhost:8088'] diff --git a/report/Dockerfile b/report/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..d131222e38617e1c16b7e0e8e265b68f253b1849 --- /dev/null +++ b/report/Dockerfile @@ -0,0 +1,3 @@ +FROM eclipse-temurin:17-jdk-alpine +COPY target/*.jar /app.jar +ENTRYPOINT ["java", "-jar", "/app.jar"] \ No newline at end of file diff --git a/report/LICENSE.md b/report/LICENSE.md new file mode 100644 index 0000000000000000000000000000000000000000..34cba43a0f19b73f2ceda1ee4ab7429e50ee365e --- /dev/null +++ b/report/LICENSE.md @@ -0,0 +1,651 @@ +GNU Affero General Public License +================================= + +_Version 3, 19 November 2007_ +_Copyright © 2007 Free Software Foundation, Inc. <<http://fsf.org/>>_ + +Everyone is permitted to copy and distribute verbatim copies +of this license document, but changing it is not allowed. + +## Preamble + +The GNU Affero General Public License is a free, copyleft license for +software and other kinds of works, specifically designed to ensure +cooperation with the community in the case of network server software. + +The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +our General Public Licenses are intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. + +When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + +Developers that use our General Public Licenses protect your rights +with two steps: **(1)** assert copyright on the software, and **(2)** offer +you this License which gives you legal permission to copy, distribute +and/or modify the software. + +A secondary benefit of defending all users' freedom is that +improvements made in alternate versions of the program, if they +receive widespread use, become available for other developers to +incorporate. Many developers of free software are heartened and +encouraged by the resulting cooperation. However, in the case of +software used on network servers, this result may fail to come about. +The GNU General Public License permits making a modified version and +letting the public access it on a server without ever releasing its +source code to the public. + +The GNU Affero General Public License is designed specifically to +ensure that, in such cases, the modified source code becomes available +to the community. It requires the operator of a network server to +provide the source code of the modified version running there to the +users of that server. Therefore, public use of a modified version, on +a publicly accessible server, gives the public access to the source +code of the modified version. + +An older license, called the Affero General Public License and +published by Affero, was designed to accomplish similar goals. This is +a different license, not a version of the Affero GPL, but Affero has +released a new version of the Affero GPL which permits relicensing under +this license. + +The precise terms and conditions for copying, distribution and +modification follow. + +## TERMS AND CONDITIONS + +### 0. Definitions + +“This License†refers to version 3 of the GNU Affero General Public License. + +“Copyright†also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + +“The Program†refers to any copyrightable work licensed under this +License. Each licensee is addressed as “youâ€. “Licensees†and +“recipients†may be individuals or organizations. + +To “modify†a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a “modified version†of the +earlier work or a work “based on†the earlier work. + +A “covered work†means either the unmodified Program or a work based +on the Program. + +To “propagate†a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + +To “convey†a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + +An interactive user interface displays “Appropriate Legal Notices†+to the extent that it includes a convenient and prominently visible +feature that **(1)** displays an appropriate copyright notice, and **(2)** +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + +### 1. Source Code + +The “source code†for a work means the preferred form of the work +for making modifications to it. “Object code†means any non-source +form of a work. + +A “Standard Interface†means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + +The “System Libraries†of an executable work include anything, other +than the work as a whole, that **(a)** is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and **(b)** serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +“Major Componentâ€, in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + +The “Corresponding Source†for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + +The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + +The Corresponding Source for a work in source code form is that +same work. + +### 2. Basic Permissions + +All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + +You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + +Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + +### 3. Protecting Users' Legal Rights From Anti-Circumvention Law + +No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + +When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + +### 4. Conveying Verbatim Copies + +You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + +You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + +### 5. Conveying Modified Source Versions + +You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + +* **a)** The work must carry prominent notices stating that you modified + it, and giving a relevant date. +* **b)** The work must carry prominent notices stating that it is + released under this License and any conditions added under section 7. + This requirement modifies the requirement in section 4 to + “keep intact all noticesâ€. +* **c)** You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. +* **d)** If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + +A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +“aggregate†if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + +### 6. Conveying Non-Source Forms + +You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + +* **a)** Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. +* **b)** Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either **(1)** a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or **(2)** access to copy the + Corresponding Source from a network server at no charge. +* **c)** Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. +* **d)** Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. +* **e)** Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + +A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + +A “User Product†is either **(1)** a “consumer productâ€, which means any +tangible personal property which is normally used for personal, family, +or household purposes, or **(2)** anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, “normally used†refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + +“Installation Information†for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + +If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + +The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + +Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + +### 7. Additional Terms + +“Additional permissions†are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + +When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + +Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + +* **a)** Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or +* **b)** Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or +* **c)** Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or +* **d)** Limiting the use for publicity purposes of names of licensors or + authors of the material; or +* **e)** Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or +* **f)** Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + +All other non-permissive additional terms are considered “further +restrictions†within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + +If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + +Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + +### 8. Termination + +You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + +However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated **(a)** +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and **(b)** permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + +Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + +Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + +### 9. Acceptance Not Required for Having Copies + +You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + +### 10. Automatic Licensing of Downstream Recipients + +Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + +An “entity transaction†is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + +You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + +### 11. Patents + +A “contributor†is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's “contributor versionâ€. + +A contributor's “essential patent claims†are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, “control†includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + +Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + +In the following three paragraphs, a “patent license†is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To “grant†such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + +If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either **(1)** cause the Corresponding Source to be so +available, or **(2)** arrange to deprive yourself of the benefit of the +patent license for this particular work, or **(3)** arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. “Knowingly relying†means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + +If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + +A patent license is “discriminatory†if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license **(a)** in connection with copies of the covered work +conveyed by you (or copies made from those copies), or **(b)** primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + +Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + +### 12. No Surrender of Others' Freedom + +If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + +### 13. Remote Network Interaction; Use with the GNU General Public License + +Notwithstanding any other provision of this License, if you modify the +Program, your modified version must prominently offer all users +interacting with it remotely through a computer network (if your version +supports such interaction) an opportunity to receive the Corresponding +Source of your version by providing access to the Corresponding Source +from a network server at no charge, through some standard or customary +means of facilitating copying of software. This Corresponding Source +shall include the Corresponding Source for any work covered by version 3 +of the GNU General Public License that is incorporated pursuant to the +following paragraph. + +Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the work with which it is combined will remain governed by version +3 of the GNU General Public License. + +### 14. Revised Versions of this License + +The Free Software Foundation may publish revised and/or new versions of +the GNU Affero General Public License from time to time. Such new versions +will be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU Affero General +Public License “or any later version†applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU Affero General Public License, you may choose any version ever published +by the Free Software Foundation. + +If the Program specifies that a proxy can decide which future +versions of the GNU Affero General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + +Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + +### 15. Disclaimer of Warranty + +THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM “AS IS†WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + +### 16. Limitation of Liability + +IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + +### 17. Interpretation of Sections 15 and 16 + +If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + +_END OF TERMS AND CONDITIONS_ + +## How to Apply These Terms to Your New Programs + +If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + +To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the “copyright†line and a pointer to where the full notice is found. + + <one line to give the program's name and a brief idea of what it does.> + Copyright (C) <year> <name of author> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. + +Also add information on how to contact you by electronic and paper mail. + +If your software can interact with users remotely through a computer +network, you should also make sure that it provides a way for users to +get its source. For example, if your program is a web application, its +interface could display a “Source†link that leads users to an archive +of the code. There are many ways you could offer source, and different +solutions will be better for different programs; see section 13 for the +specific requirements. + +You should also get your employer (if you work as a programmer) or school, +if any, to sign a “copyright disclaimer†for the program, if necessary. +For more information on this, and how to apply and follow the GNU AGPL, see +<<http://www.gnu.org/licenses/>>. \ No newline at end of file diff --git a/report/openapi.yaml b/report/openapi.yaml index 5d4b521de2e8d3a1ddee8746fc4c75ca871eaea8..487f7a9a85290dc90577c7892886d352baed4919 100644 --- a/report/openapi.yaml +++ b/report/openapi.yaml @@ -27,6 +27,8 @@ servers: tags: - name: Report description: Microservice for report pdf report. +security: + - BearerAuth: [] paths: /api/reports/flight/{id}: get: @@ -57,6 +59,12 @@ paths: application/json: schema: $ref: '../core/openapi.yaml#/components/schemas/ErrorMessage' + "500": + description: Internal server error + content: + application/json: + schema: + $ref: '../core/openapi.yaml#/components/schemas/ErrorMessage' /api/reports/airport/{id}: get: @@ -87,6 +95,12 @@ paths: application/json: schema: $ref: '../core/openapi.yaml#/components/schemas/ErrorMessage' + "500": + description: Internal server error + content: + application/json: + schema: + $ref: '../core/openapi.yaml#/components/schemas/ErrorMessage' /api/reports/airplane/{id}: get: @@ -116,4 +130,16 @@ paths: content: application/json: schema: - $ref: '../core/openapi.yaml#/components/schemas/ErrorMessage' \ No newline at end of file + $ref: '../core/openapi.yaml#/components/schemas/ErrorMessage' + "500": + description: Internal server error + content: + application/json: + schema: + $ref: '../core/openapi.yaml#/components/schemas/ErrorMessage' +components: + securitySchemes: + BearerAuth: + type: http + description: "OAuth2 Resource Server, provide a valid access token" + scheme: bearer diff --git a/report/pom.xml b/report/pom.xml index cec25612c55c5f8820a7d635cf33eab759bd23d9..8594269c13bf2e55d23d39706ca63f2b98e87122 100644 --- a/report/pom.xml +++ b/report/pom.xml @@ -21,6 +21,18 @@ <scope>runtime</scope> </dependency> + <!-- Actuator --> + <dependency> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-starter-actuator</artifactId> + </dependency> + + <!-- Prometheus --> + <dependency> + <groupId>io.micrometer</groupId> + <artifactId>micrometer-registry-prometheus</artifactId> + </dependency> + <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> @@ -41,11 +53,6 @@ <artifactId>jakarta.annotation-api</artifactId> </dependency> - <dependency> - <groupId>jakarta.validation</groupId> - <artifactId>jakarta.validation-api</artifactId> - </dependency> - <dependency> <groupId>io.swagger.core.v3</groupId> <artifactId>swagger-models-jakarta</artifactId> @@ -62,13 +69,14 @@ </dependency> <dependency> - <groupId>javax.validation</groupId> - <artifactId>validation-api</artifactId> + <groupId>org.springdoc</groupId> + <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId> </dependency> <dependency> - <groupId>org.springdoc</groupId> - <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId> + <groupId>cz.muni.fi.pa165</groupId> + <artifactId>user-client</artifactId> + <version>1.0-SNAPSHOT</version> </dependency> <!-- for pagination from JPA without actually using JPA --> @@ -89,18 +97,34 @@ <artifactId>itextpdf</artifactId> <version>5.5.10</version> </dependency> - <dependency> <groupId>org.apache.pdfbox</groupId> <artifactId>pdfbox</artifactId> <version>2.0.4</version> </dependency> + <dependency> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-starter-oauth2-resource-server</artifactId> + </dependency> + <dependency> <groupId>org.bouncycastle</groupId> <artifactId>bcprov-jdk15on</artifactId> <version>1.56</version> </dependency> + + <dependency> + <groupId>cz.muni.fi.pa165</groupId> + <artifactId>core-client</artifactId> + <version>1.0-SNAPSHOT</version> + </dependency> + + <dependency> + <groupId>cz.muni.fi.pa165</groupId> + <artifactId>user-client</artifactId> + <version>1.0-SNAPSHOT</version> + </dependency> </dependencies> <build> diff --git a/report/src/main/java/cz/muni/fi/pa165/report/server/ReportController.java b/report/src/main/java/cz/muni/fi/pa165/report/server/ReportController.java deleted file mode 100644 index 4e0ed11a53822f7cf4470017e836e8d854e042c9..0000000000000000000000000000000000000000 --- a/report/src/main/java/cz/muni/fi/pa165/report/server/ReportController.java +++ /dev/null @@ -1,49 +0,0 @@ -package cz.muni.fi.pa165.report.server; -import cz.muni.fi.pa165.report.server.api.ReportApiDelegate; -import org.springframework.core.io.ByteArrayResource; -import org.springframework.core.io.Resource; -import org.springframework.http.HttpHeaders; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.RestController; - -import java.io.*; - - -@RestController -public class ReportController implements ReportApiDelegate{ - - @Override - public ResponseEntity<Resource> getReportFlightById(Long id){ - - ClassLoader classLoader = getClass().getClassLoader(); - - HttpHeaders headers = new HttpHeaders(); - headers.add("Content-Type", "application/pdf"); - headers.add("Content-Disposition", "attachment; filename=labels.pdf"); - - // the stream holding the file content - - // funny, if can use Java 7, please uses Files.readAllBytes(path) - try(InputStream inputStream = classLoader.getResourceAsStream("sample.pdf")){ - byte[] bytes = new byte[inputStream.available()]; - inputStream.read(bytes); - ByteArrayResource resource = new ByteArrayResource(bytes); - return new ResponseEntity<>(resource, headers, HttpStatus.OK); - } catch (IOException e) { - throw new RuntimeException(e); - } - } - - @Override - public ResponseEntity<Resource> getReportAirportById(Long id){ - - return getReportFlightById(id); - } - @Override - public ResponseEntity<Resource> getReportAirplaneById(Long id){ - - return getReportFlightById(id); - } -} - diff --git a/report/src/main/java/cz/muni/fi/pa165/report/server/ReportType.java b/report/src/main/java/cz/muni/fi/pa165/report/server/ReportType.java new file mode 100644 index 0000000000000000000000000000000000000000..302271f3eec33a75c886a53d12fd38ed3a267132 --- /dev/null +++ b/report/src/main/java/cz/muni/fi/pa165/report/server/ReportType.java @@ -0,0 +1,5 @@ +package cz.muni.fi.pa165.report.server; + +public enum ReportType { + AIRPLANE, AIRPORT, FLIGHT +} diff --git a/report/src/main/java/cz/muni/fi/pa165/report/server/config/AppConfig.java b/report/src/main/java/cz/muni/fi/pa165/report/server/config/AppConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..e5c3a4299a9aeb80b84b467e021bf24e847194d0 --- /dev/null +++ b/report/src/main/java/cz/muni/fi/pa165/report/server/config/AppConfig.java @@ -0,0 +1,31 @@ +package cz.muni.fi.pa165.report.server.config; + +import cz.muni.fi.pa165.user.client.Authorities; +import cz.muni.fi.pa165.user.client.UserServiceInterceptionConfigurer; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.http.HttpMethod; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configurers.oauth2.server.resource.OAuth2ResourceServerConfigurer; +import org.springframework.security.web.SecurityFilterChain; + +@Configuration +@Import(UserServiceInterceptionConfigurer.class) +public class AppConfig { + + @Bean + SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { + http.authorizeHttpRequests(x -> x + // swagger: + .requestMatchers("/swagger-ui/**").permitAll() + .requestMatchers("/v3/api-docs/**").permitAll() + .requestMatchers(HttpMethod.GET, "/").permitAll() + .requestMatchers(HttpMethod.GET, "/swagger-ui.html").permitAll() + // Manager has access to all reports + .anyRequest().hasAuthority(Authorities.MANAGER) + ) + .oauth2ResourceServer(OAuth2ResourceServerConfigurer::opaqueToken); + return http.build(); + } +} diff --git a/report/src/main/java/cz/muni/fi/pa165/report/server/exceptions/InternalServerErrorAdvice.java b/report/src/main/java/cz/muni/fi/pa165/report/server/exceptions/InternalServerErrorAdvice.java new file mode 100644 index 0000000000000000000000000000000000000000..ead20c06a22c89bd5a84d05c66ff3abdefbeedbf --- /dev/null +++ b/report/src/main/java/cz/muni/fi/pa165/report/server/exceptions/InternalServerErrorAdvice.java @@ -0,0 +1,27 @@ +package cz.muni.fi.pa165.report.server.exceptions; + +import cz.muni.fi.pa165.report.server.model.ErrorMessage; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.servlet.support.ServletUriComponentsBuilder; + +import java.time.OffsetDateTime; + +@ControllerAdvice +public class InternalServerErrorAdvice { + + @ExceptionHandler(ResourceNotFoundException.class) + @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) + ResponseEntity<ErrorMessage> handleInternalServerErrorException(InternalServerErrorException ex) { + ErrorMessage errorMessage = new ErrorMessage(); + errorMessage.setTimestamp(OffsetDateTime.now()); + errorMessage.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value()); + errorMessage.setError(HttpStatus.INTERNAL_SERVER_ERROR.getReasonPhrase()); + errorMessage.setMessage(ex.getMessage()); + errorMessage.setPath(ServletUriComponentsBuilder.fromCurrentRequestUri().toUriString()); + return new ResponseEntity<>(errorMessage, HttpStatus.INTERNAL_SERVER_ERROR); + } +} diff --git a/report/src/main/java/cz/muni/fi/pa165/report/server/exceptions/InternalServerErrorException.java b/report/src/main/java/cz/muni/fi/pa165/report/server/exceptions/InternalServerErrorException.java new file mode 100644 index 0000000000000000000000000000000000000000..55166249371715d43ab9bcc8fb84d2a5571a377f --- /dev/null +++ b/report/src/main/java/cz/muni/fi/pa165/report/server/exceptions/InternalServerErrorException.java @@ -0,0 +1,24 @@ +package cz.muni.fi.pa165.report.server.exceptions; + +public class InternalServerErrorException extends RuntimeException { + + public InternalServerErrorException() { + super("Internal Server Error"); + } + + public InternalServerErrorException(String message) { + super(message); + } + + public InternalServerErrorException(String message, Throwable cause) { + super(message, cause); + } + + public InternalServerErrorException(Throwable cause) { + super(cause); + } + + public InternalServerErrorException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) { + super(message, cause, enableSuppression, writableStackTrace); + } +} diff --git a/report/src/main/java/cz/muni/fi/pa165/report/server/exceptions/ResourceNotFoundAdvice.java b/report/src/main/java/cz/muni/fi/pa165/report/server/exceptions/ResourceNotFoundAdvice.java new file mode 100644 index 0000000000000000000000000000000000000000..f92a78bca368a7b48c40d9beee2b5ce3d519c03c --- /dev/null +++ b/report/src/main/java/cz/muni/fi/pa165/report/server/exceptions/ResourceNotFoundAdvice.java @@ -0,0 +1,27 @@ +package cz.muni.fi.pa165.report.server.exceptions; + +import cz.muni.fi.pa165.report.server.model.ErrorMessage; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.servlet.support.ServletUriComponentsBuilder; + +import java.time.OffsetDateTime; + +@ControllerAdvice +public class ResourceNotFoundAdvice { + + @ExceptionHandler(ResourceNotFoundException.class) + @ResponseStatus(HttpStatus.NOT_FOUND) + ResponseEntity<ErrorMessage> handleResourceNotFoundException(ResourceNotFoundException ex) { + ErrorMessage errorMessage = new ErrorMessage(); + errorMessage.setTimestamp(OffsetDateTime.now()); + errorMessage.setStatus(HttpStatus.NOT_FOUND.value()); + errorMessage.setError(HttpStatus.NOT_FOUND.getReasonPhrase()); + errorMessage.setMessage(ex.getMessage()); + errorMessage.setPath(ServletUriComponentsBuilder.fromCurrentRequestUri().toUriString()); + return new ResponseEntity<>(errorMessage, HttpStatus.NOT_FOUND); + } +} diff --git a/report/src/main/java/cz/muni/fi/pa165/report/server/exceptions/ResourceNotFoundException.java b/report/src/main/java/cz/muni/fi/pa165/report/server/exceptions/ResourceNotFoundException.java new file mode 100644 index 0000000000000000000000000000000000000000..2a48981c59af402fef4edd4145ae1449f97b48bb --- /dev/null +++ b/report/src/main/java/cz/muni/fi/pa165/report/server/exceptions/ResourceNotFoundException.java @@ -0,0 +1,24 @@ +package cz.muni.fi.pa165.report.server.exceptions; + +public class ResourceNotFoundException extends RuntimeException { + + public ResourceNotFoundException() { + super("Resource Not Found"); + } + + public ResourceNotFoundException(String message) { + super(message); + } + + public ResourceNotFoundException(String message, Throwable cause) { + super(message, cause); + } + + public ResourceNotFoundException(Throwable cause) { + super(cause); + } + + public ResourceNotFoundException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) { + super(message, cause, enableSuppression, writableStackTrace); + } +} diff --git a/report/src/main/java/cz/muni/fi/pa165/report/server/facade/ReportFacade.java b/report/src/main/java/cz/muni/fi/pa165/report/server/facade/ReportFacade.java new file mode 100644 index 0000000000000000000000000000000000000000..32a1712751e17f3e52d97598f0f90d8ea4080d13 --- /dev/null +++ b/report/src/main/java/cz/muni/fi/pa165/report/server/facade/ReportFacade.java @@ -0,0 +1,12 @@ +package cz.muni.fi.pa165.report.server.facade; + +import org.springframework.core.io.Resource; + +public interface ReportFacade { + + Resource getReportFlightById(Long id); + + Resource getReportAirportById(Long id); + + Resource getReportAirplaneById(Long id); +} diff --git a/report/src/main/java/cz/muni/fi/pa165/report/server/facade/ReportFacadeImpl.java b/report/src/main/java/cz/muni/fi/pa165/report/server/facade/ReportFacadeImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..a84d5c3cbce71cd69454ba2634eb9b31f292e01a --- /dev/null +++ b/report/src/main/java/cz/muni/fi/pa165/report/server/facade/ReportFacadeImpl.java @@ -0,0 +1,176 @@ +package cz.muni.fi.pa165.report.server.facade; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import cz.muni.fi.pa165.core.client.*; +import cz.muni.fi.pa165.core.client.invoker.ApiClient; +import cz.muni.fi.pa165.core.client.invoker.ApiException; +import cz.muni.fi.pa165.core.client.model.*; +import cz.muni.fi.pa165.report.server.ReportType; +import cz.muni.fi.pa165.report.server.exceptions.ResourceNotFoundException; +import cz.muni.fi.pa165.report.server.service.ReportDocumentService; +import cz.muni.fi.pa165.user.client.UserApi; +import org.springframework.core.io.ByteArrayResource; +import org.springframework.core.io.Resource; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.oauth2.core.OAuth2AccessToken; +import org.springframework.security.oauth2.server.resource.authentication.BearerTokenAuthentication; +import org.springframework.security.oauth2.server.resource.introspection.OAuth2IntrospectionAuthenticatedPrincipal; +import org.springframework.stereotype.Service; + +@Service +public class ReportFacadeImpl implements ReportFacade{ + + private final ReportDocumentService reportDocumentService; + Map<String, List<String>> documentData; + + public ReportFacadeImpl(ReportDocumentService reportDocumentService){ + this.reportDocumentService = reportDocumentService; + this.documentData = new HashMap<>(); + } + + ApiClient getApiClient() { + var client = new ApiClient(); + client.setRequestInterceptor((builder -> { + var authentication = SecurityContextHolder.getContext().getAuthentication(); + var authPrincipal = (OAuth2IntrospectionAuthenticatedPrincipal) authentication.getPrincipal(); + var token = (OAuth2AccessToken) (((BearerTokenAuthentication) authentication).getToken()); + builder.header("Authorization", "Bearer " + token.getTokenValue()); + })); + return client; + } + + @Override + public Resource getReportAirportById(Long id){ + + AirportDto airport; + CityDto cityDto; + CountryDto countryDto; + + var client = getApiClient(); + CountryApi countryApi = new CountryApi(client); + CityApi cityApi = new CityApi(client); + AirportApi airportApi = new AirportApi(client); + List<String> airportData = new ArrayList<>(); + List<String> cityData = new ArrayList<>(); + + //Getting airport data and then calling service creating pdf document from them + try { + airport = airportApi.getAirportById(id); + + airportData.add("Airport " + airport.getName()); + airportData.add("The code of airport is " + airport.getCode()); + airportData.add("Location of airport " + airport.getName() + " is: \n" + + airport.getLocation()); + documentData.put("airport", airportData); + + if(airport.getCityId() != null && airport.getCityId() != 0) { + cityDto = cityApi.getCityById(airport.getCityId()); + cityData.add("Airport is located in the city " + cityDto.getName()); + + if (cityDto.getCountryId() != null && cityDto.getCountryId() != 0){ + countryDto = countryApi.getCountryById(cityDto.getCountryId()); + cityData.add("Country of this city is " + countryDto.getName()); + } + } + documentData.put("airportCity", cityData); + + + } catch (ApiException e) { + throw new ResourceNotFoundException(e); + } + return new ByteArrayResource(reportDocumentService.reportDocument(documentData, "test", ReportType.AIRPORT).toByteArray()); + } + + @Override + public Resource getReportFlightById(Long id){ + var client = getApiClient(); + FlightApi flightApi = new FlightApi(client); + StewardApi stewardApi = new StewardApi(client); + AirplaneApi airplaneApi = new AirplaneApi(client); + AirportApi airportApi = new AirportApi(client); + AirplaneDto airplaneDto; + AirportDto arrivalAirportDto; + AirportDto departureAirportDto; + FlightDto flight; + List<String> flightData = new ArrayList<>(); + List<String> stewardsData = new ArrayList<>(); + List<String> airplaneData = new ArrayList<>(); + List<String> airportsData = new ArrayList<>(); + + //Getting flight data and then calling service creating pdf document from them + try { + flight = flightApi.getFlightById(id); + + flightData.add("Arrival time of flight is " + flight.getArrivalTime()); + flightData.add("Departure time of flight is " + flight.getDepartureTime()); + documentData.put("flight", flightData); + + if( flight.getAssignedStewardIds() != null){ + flight.getAssignedStewardIds().forEach(stewardId -> + { + StewardDto stewardDto; + try { + stewardDto = stewardApi.getSteward(stewardId); + } catch (ApiException e) { + throw new ResourceNotFoundException(e); + } + stewardsData.add(stewardDto.getFirstName() + " " + stewardDto.getLastName()); + }); + } + documentData.put("stewards", stewardsData); + + if(flight.getAirplaneId() != null && flight.getAirplaneId() != 0) { + airplaneDto = airplaneApi.getAirplaneById(flight.getAirplaneId()); + airplaneData.add("Airplane name is " + airplaneDto.getName()); + airplaneData.add("Airplane capacity is " + airplaneDto.getCapacity()); + } + documentData.put("airplane", airplaneData); + + if (flight.getArrivalAirportId() != null && flight.getArrivalAirportId() != 0) { + arrivalAirportDto = airportApi.getAirportById(flight.getArrivalAirportId()); + airportsData.add("Arrival airport is " + arrivalAirportDto.getName()); + } + if (flight.getDepartureAirportId() != null && flight.getDepartureAirportId() != 0) { + departureAirportDto = airportApi.getAirportById(flight.getDepartureAirportId()); + airportsData.add("Departure airport is " + departureAirportDto.getName()); + } + documentData.put("airports", airportsData); + + } catch (ApiException e) { + throw new ResourceNotFoundException(e); + } + return new ByteArrayResource(reportDocumentService.reportDocument(documentData, "Flight", ReportType.FLIGHT).toByteArray()); + } + + @Override + public Resource getReportAirplaneById(Long id){ + var client = getApiClient(); + AirplaneApi airplaneApi = new AirplaneApi(client); + AirplaneTypeApi airplaneTypeApi = new AirplaneTypeApi(client); + AirplaneTypeDto airplaneTypeDto; + AirplaneDto airplane; + List<String> airplaneData = new ArrayList<>(); + List<String> airplaneTypeData = new ArrayList<>(); + + //Getting airplane data and then calling service creating pdf document from them + try { + airplane = airplaneApi.getAirplaneById(id); + + airplaneData.add("Airplane name: " + airplane.getName()); + airplaneData.add("Airplane capacity: " + airplane.getCapacity()); + documentData.put("airplane", airplaneData); + + airplaneTypeDto = airplaneTypeApi.getAirplaneTypeById(airplane.getTypeId()); + airplaneTypeData.add("Airplane type name: " + airplaneTypeDto.getName()); + documentData.put("airplaneType", airplaneTypeData); + + } catch (ApiException e) { + throw new ResourceNotFoundException(e); + } + return new ByteArrayResource(reportDocumentService.reportDocument(documentData, "Airplane", ReportType.AIRPLANE).toByteArray()); + } +} diff --git a/report/src/main/java/cz/muni/fi/pa165/report/server/rest/ReportController.java b/report/src/main/java/cz/muni/fi/pa165/report/server/rest/ReportController.java new file mode 100644 index 0000000000000000000000000000000000000000..2cf3b9ee7f7b7f7828ce61b7df68dc1c1a6ed810 --- /dev/null +++ b/report/src/main/java/cz/muni/fi/pa165/report/server/rest/ReportController.java @@ -0,0 +1,54 @@ +package cz.muni.fi.pa165.report.server.rest; +import cz.muni.fi.pa165.report.server.api.ReportApiDelegate; +import cz.muni.fi.pa165.report.server.facade.ReportFacade; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.core.io.Resource; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.RestController; + + +@RestController +public class ReportController implements ReportApiDelegate{ + private final ReportFacade reportFacade; + @Autowired + public ReportController(ReportFacade reportFacade) { + this.reportFacade = reportFacade; + } + HttpHeaders headers = new HttpHeaders(); + private HttpHeaders initializeHeaders(){ + headers.clear(); + headers.add("Content-Type", "application/pdf"); + headers.add("Content-Disposition", "attachment; filename=report.pdf"); + return headers; + } + @Override + public ResponseEntity<Resource> getReportFlightById(Long id){ + + Resource resource = reportFacade.getReportFlightById(id); + + headers = initializeHeaders(); + + return new ResponseEntity<>(resource, headers, HttpStatus.OK); + } + @Override + public ResponseEntity<Resource> getReportAirportById(Long id){ + + Resource resource = reportFacade.getReportAirportById(id); + + headers = initializeHeaders(); + + return new ResponseEntity<>(resource, headers, HttpStatus.OK); + } + @Override + public ResponseEntity<Resource> getReportAirplaneById(Long id){ + + Resource resource = reportFacade.getReportAirplaneById(id); + + headers = initializeHeaders(); + + return new ResponseEntity<>(resource, headers, HttpStatus.OK); + } +} + diff --git a/report/src/main/java/cz/muni/fi/pa165/report/server/service/ReportDocumentService.java b/report/src/main/java/cz/muni/fi/pa165/report/server/service/ReportDocumentService.java new file mode 100644 index 0000000000000000000000000000000000000000..c76044329542021a580df9ab0624cf75b1fcd7fe --- /dev/null +++ b/report/src/main/java/cz/muni/fi/pa165/report/server/service/ReportDocumentService.java @@ -0,0 +1,11 @@ +package cz.muni.fi.pa165.report.server.service; + +import cz.muni.fi.pa165.report.server.ReportType; + +import java.io.ByteArrayOutputStream; +import java.util.List; +import java.util.Map; + +public interface ReportDocumentService { + ByteArrayOutputStream reportDocument(Map<String, List<String>> data, String name, ReportType reportType); +} diff --git a/report/src/main/java/cz/muni/fi/pa165/report/server/service/ReportDocumentServiceImpl.java b/report/src/main/java/cz/muni/fi/pa165/report/server/service/ReportDocumentServiceImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..89bb0d1f858f727e6be81d6cacfbe3125ecebc31 --- /dev/null +++ b/report/src/main/java/cz/muni/fi/pa165/report/server/service/ReportDocumentServiceImpl.java @@ -0,0 +1,121 @@ +package cz.muni.fi.pa165.report.server.service; + +import com.itextpdf.text.*; +import java.util.List; +import com.itextpdf.text.pdf.PdfWriter; +import cz.muni.fi.pa165.report.server.ReportType; +import cz.muni.fi.pa165.report.server.exceptions.InternalServerErrorException; +import org.springframework.stereotype.Service; +import java.io.ByteArrayOutputStream; +import java.util.Map; + +@Service +public class ReportDocumentServiceImpl implements ReportDocumentService{ + + private static final Font CATFONT = new Font(Font.FontFamily.TIMES_ROMAN, 18, + Font.BOLD); + private static final Font SUBFONT = new Font(Font.FontFamily.TIMES_ROMAN, 16, + Font.BOLD); + + @Override + public ByteArrayOutputStream reportDocument(Map<String, List<String>> data, String name, ReportType reportType) { + // Creating empty document and according to type calling methods inserting data to it + com.itextpdf.text.Document document = new com.itextpdf.text.Document(PageSize.A4, 50, 50, 50, 50); + ByteArrayOutputStream out = new ByteArrayOutputStream(); + PdfWriter writer; + try { + writer = PdfWriter.getInstance(document, out); + } catch (DocumentException e) { + throw new InternalServerErrorException(e); + } + document.open(); + try { + addMetaData(document, name, name, name); + switch (reportType) { + case AIRPLANE -> airplaneData(document, data); + case FLIGHT -> flightData(document, data); + case AIRPORT -> airportData(document, data); + } + } + catch (DocumentException e) { + throw new InternalServerErrorException(e); + } + document.close(); + writer.close(); + return out; + } + + private static void airplaneData(Document document, Map<String, List<String>> data) throws DocumentException{ + newDataAnchor(document, data.get("airplane"), "Airplane", 1); + newDataAnchor(document, data.get("airplaneType"), "Airplane type", 2); + } + + private static void flightData(Document document, Map<String, List<String>> data) + throws DocumentException{ + newDataAnchor(document, data.get("flight"), "Flight", 1); + newListAnchor(document, data.get("stewards"), "Stewards", 2); + newDataAnchor(document, data.get("airplane"), "Airplane", 3); + newDataAnchor(document, data.get("airports"), "Airports", 4); + } + + private static void airportData(Document document, Map<String, List<String>> data) throws DocumentException{ + newDataAnchor(document, data.get("airport"), "Airport", 1); + newDataAnchor(document, data.get("airportCity"), "Airport city", 2); + } + + private static void createList(Section section, List<String> listData) { + // Creating pdf document list from data + com.itextpdf.text.List list = new com.itextpdf.text.List(true, false, 10); + if (listData != null) { + listData.forEach(list::add); + } + section.add(list); + } + + private static void addMetaData(Document document, String title, String subject, String keywords) { + document.addTitle(title); + document.addSubject(subject); + document.addKeywords(keywords); + document.addAuthor("PA165 - Airport Manager"); + document.addCreator("iText"); + } + + private static void addEmptyLines(Paragraph paragraph, int numberOfLines) { + for (int i = 0; i < numberOfLines; i++) { + paragraph.add(new Paragraph(" ")); + } + } + + private static void newDataAnchor (Document document, List<String> data, String name, Integer chapterNumber) + throws DocumentException{ + Anchor anchor = new Anchor(name, CATFONT); + anchor.setName(name); + + Chapter chapter = new Chapter(new Paragraph(anchor), chapterNumber); + Section dataSection= chapter.addSection(new Paragraph(name + " data", SUBFONT)); + if(data != null) { + data.forEach(value -> + dataSection.add(new Paragraph(value))); + } + Paragraph paragraph = new Paragraph(); + addEmptyLines(paragraph, 5); + dataSection.add(paragraph); + //adding all created components to the document + document.add(chapter); + } + + private static void newListAnchor (Document document, List<String> data, String name, Integer chapterNumber) + throws DocumentException{ + Anchor anchor = new Anchor(name, CATFONT); + anchor.setName(name); + + Chapter chapter = new Chapter(new Paragraph(anchor), chapterNumber); + Section listSection= chapter.addSection(new Paragraph(name + " data", SUBFONT)); + createList(listSection, data); + Paragraph paragraph = new Paragraph(); + addEmptyLines(paragraph, 5); + listSection.add(paragraph); + // now add all this to the document + document.add(chapter); + } +} diff --git a/report/src/main/resources/application.yaml b/report/src/main/resources/application.yaml index 0abcd741f27e93ab0d76b682b58e413b874d2c92..f0c91d878b6ce53ccba20a1f0a6ba47aad0c345c 100644 --- a/report/src/main/resources/application.yaml +++ b/report/src/main/resources/application.yaml @@ -6,7 +6,35 @@ spring: driverClassName: org.h2.Driver jpa: database-platform: org.hibernate.dialect.H2Dialect + security: + oauth2: + resourceserver: + opaque-token: + introspection-uri: https://oidc.muni.cz/oidc/introspect + # Martin Kuba's testing resource server + client-id: d57b3a8f-156e-46de-9f27-39c4daee05e1 + client-secret: fa228ebc-4d54-4cda-901e-4d6287f8b1652a9c9c44-73c9-4502-973f-bcdb4a8ec96a # Let's make weather microservice run on port 8088 # for now to not collide with another microservices server: port: 8085 +management: + endpoints: + web: + exposure: + include: 'info,health,metrics,prometheus' + info: + env: + enabled: true + endpoint: + health: + show-details: always + show-components: always + probes: + enabled: true +info: + app: + encoding: 'UTF-8' + java: + source: '17' + target: '17' diff --git a/report/src/main/resources/report(1).pdf b/report/src/main/resources/report(1).pdf new file mode 100644 index 0000000000000000000000000000000000000000..669890a9711a69ae10be8e60dd475f79ceae28e4 Binary files /dev/null and b/report/src/main/resources/report(1).pdf differ diff --git a/report/src/main/resources/report.pdf b/report/src/main/resources/report.pdf new file mode 100644 index 0000000000000000000000000000000000000000..7ea01b7d8820fbe706c6949fd7e4c7674319d149 Binary files /dev/null and b/report/src/main/resources/report.pdf differ diff --git a/report/src/main/resources/reportAirplane.pdf b/report/src/main/resources/reportAirplane.pdf new file mode 100644 index 0000000000000000000000000000000000000000..47578b65448a91e14fbf9aa53dd393c94edc16b7 Binary files /dev/null and b/report/src/main/resources/reportAirplane.pdf differ diff --git a/report/src/main/resources/sample.pdf b/report/src/main/resources/sample.pdf deleted file mode 100644 index c0e31a076aeb1fa7729e82279943b3504f85338d..0000000000000000000000000000000000000000 --- a/report/src/main/resources/sample.pdf +++ /dev/null @@ -1,198 +0,0 @@ -%PDF-1.3 -%âãÏÓ - -1 0 obj -<< -/Type /Catalog -/Outlines 2 0 R -/Pages 3 0 R ->> -endobj - -2 0 obj -<< -/Type /Outlines -/Count 0 ->> -endobj - -3 0 obj -<< -/Type /Pages -/Count 2 -/Kids [ 4 0 R 6 0 R ] ->> -endobj - -4 0 obj -<< -/Type /Page -/Parent 3 0 R -/Resources << -/Font << -/F1 9 0 R ->> -/ProcSet 8 0 R ->> -/MediaBox [0 0 612.0000 792.0000] -/Contents 5 0 R ->> -endobj - -5 0 obj -<< /Length 1074 >> -stream -2 J -BT -0 0 0 rg -/F1 0027 Tf -57.3750 722.2800 Td -( A Simple PDF File ) Tj -ET -BT -/F1 0010 Tf -69.2500 688.6080 Td -( This is a small demonstration .pdf file - ) Tj -ET -BT -/F1 0010 Tf -69.2500 664.7040 Td -( just for use in the Virtual Mechanics tutorials. More text. And more ) Tj -ET -BT -/F1 0010 Tf -69.2500 652.7520 Td -( text. And more text. And more text. And more text. ) Tj -ET -BT -/F1 0010 Tf -69.2500 628.8480 Td -( And more text. And more text. And more text. And more text. And more ) Tj -ET -BT -/F1 0010 Tf -69.2500 616.8960 Td -( text. And more text. Boring, zzzzz. And more text. And more text. And ) Tj -ET -BT -/F1 0010 Tf -69.2500 604.9440 Td -( more text. And more text. And more text. And more text. And more text. ) Tj -ET -BT -/F1 0010 Tf -69.2500 592.9920 Td -( And more text. And more text. ) Tj -ET -BT -/F1 0010 Tf -69.2500 569.0880 Td -( And more text. And more text. And more text. And more text. And more ) Tj -ET -BT -/F1 0010 Tf -69.2500 557.1360 Td -( text. And more text. And more text. Even more. Continued on page 2 ...) Tj -ET -endstream -endobj - -6 0 obj -<< -/Type /Page -/Parent 3 0 R -/Resources << -/Font << -/F1 9 0 R ->> -/ProcSet 8 0 R ->> -/MediaBox [0 0 612.0000 792.0000] -/Contents 7 0 R ->> -endobj - -7 0 obj -<< /Length 676 >> -stream -2 J -BT -0 0 0 rg -/F1 0027 Tf -57.3750 722.2800 Td -( Simple PDF File 2 ) Tj -ET -BT -/F1 0010 Tf -69.2500 688.6080 Td -( ...continued from page 1. Yet more text. And more text. And more text. ) Tj -ET -BT -/F1 0010 Tf -69.2500 676.6560 Td -( And more text. And more text. And more text. And more text. And more ) Tj -ET -BT -/F1 0010 Tf -69.2500 664.7040 Td -( text. Oh, how boring typing this stuff. But not as boring as watching ) Tj -ET -BT -/F1 0010 Tf -69.2500 652.7520 Td -( paint dry. And more text. And more text. And more text. And more text. ) Tj -ET -BT -/F1 0010 Tf -69.2500 640.8000 Td -( Boring. More, a little more text. The end, and just as well. ) Tj -ET -endstream -endobj - -8 0 obj -[/PDF /Text] -endobj - -9 0 obj -<< -/Type /Font -/Subtype /Type1 -/Name /F1 -/BaseFont /Helvetica -/Encoding /WinAnsiEncoding ->> -endobj - -10 0 obj -<< -/Creator (Rave \(http://www.nevrona.com/rave\)) -/Producer (Nevrona Designs) -/CreationDate (D:20060301072826) ->> -endobj - -xref -0 11 -0000000000 65535 f -0000000019 00000 n -0000000093 00000 n -0000000147 00000 n -0000000222 00000 n -0000000390 00000 n -0000001522 00000 n -0000001690 00000 n -0000002423 00000 n -0000002456 00000 n -0000002574 00000 n - -trailer -<< -/Size 11 -/Root 1 0 R -/Info 10 0 R ->> - -startxref -2714 -%%EOF diff --git a/report/src/test/java/cz/muni/fi/pa165/report/server/ReportIT.java b/report/src/test/java/cz/muni/fi/pa165/report/server/ReportIT.java index 99c343405652f8df59637f9749daa1bd7a105df5..47edce289ce4a925f376337b76ebf74d0695a764 100644 --- a/report/src/test/java/cz/muni/fi/pa165/report/server/ReportIT.java +++ b/report/src/test/java/cz/muni/fi/pa165/report/server/ReportIT.java @@ -1,99 +1,127 @@ package cz.muni.fi.pa165.report.server; + +import cz.muni.fi.pa165.report.server.service.ReportDocumentService; +import cz.muni.fi.pa165.report.server.service.ReportDocumentServiceImpl; import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; - -import com.fasterxml.jackson.databind.ObjectMapper;; +import com.fasterxml.jackson.databind.ObjectMapper; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; -import org.springframework.core.io.ByteArrayResource; -import org.springframework.test.web.servlet.MockMvc; - -import java.io.IOException; -import java.io.InputStream; -import java.util.Arrays; - +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; import static org.assertj.core.api.Assertions.assertThat; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + /** * Integration tests. Run by "maven verify". */ @SpringBootTest -@AutoConfigureMockMvc +@AutoConfigureMockMvc(addFilters = false) class ReportIT { private static final Logger log = LoggerFactory.getLogger(ReportIT.class); - @Autowired - private MockMvc mockMvc; + ReportDocumentService reportDocumentService = new ReportDocumentServiceImpl(); + + Map<String, List<String>> documentData = new HashMap<>(); @Autowired ObjectMapper objectMapper; @Test - void getReportFlightByIdTest() throws Exception { + void getReportFlightByIdTest(){ log.debug("getReportFlightByIdTest() running"); + List<String> flightData = new ArrayList<>(); + List<String> stewardsData; + List<String> airplaneData = new ArrayList<>(); + List<String> airportsData = new ArrayList<>(); + + flightData.add("Arrival time of flight is 23:00"); + flightData.add("Departure time of flight is 17:00"); + documentData.put("flight", flightData); + + stewardsData = List.of(new String[]{"Adam KrÃdl", "Matej Hrica", "Martin SlovÃk", "Ján MacháÄek"}); + documentData.put("stewards", stewardsData); + + + airplaneData.add("Airplane name is TestPlane"); + airplaneData.add("Airplane capacity is 2000"); + + documentData.put("airplane", airplaneData); + + airportsData.add("Arrival airport is Arrival Test Airport"); + airportsData.add("Departure airport is Departure Test Airport"); + + documentData.put("airports", airportsData); + + var document = reportDocumentService.reportDocument(documentData, "flight test", ReportType.FLIGHT); - ClassLoader classLoader = getClass().getClassLoader(); - - try(InputStream inputStream = classLoader.getResourceAsStream("sample.pdf")){ - byte[] bytes = new byte[inputStream.available()]; - inputStream.read(bytes); - ByteArrayResource resource = new ByteArrayResource(bytes); - var response = mockMvc.perform(get("/api/reports/flight/1")) - .andExpect(status().isOk()) - .andReturn().getResponse().getContentAsByteArray(); - assertThat(Arrays.equals(response, resource.getByteArray())); - log.debug("response: {}", response); - } catch (IOException e) { - throw new RuntimeException(e); - } + assertThat(document.toString()).contains("Type/Pages/Count 4"); + assertThat(document.toString()).contains("Flight"); + assertThat(document.toString()).contains("Stewards"); + assertThat(document.toString()).contains("Airplane"); + assertThat(document.toString()).contains("Airports"); + assertThat(document.toString()).contains("flight test"); + + log.debug("Document: {}", document); } @Test - void getReportAirportByIdTest() throws Exception { - log.debug("getReportFlightByIdTest() running"); + void getReportAirportByIdTest(){ + log.debug("getReportAirportByIdTest() running"); + List<String> airportData = new ArrayList<>(); + List<String> airportCityData = new ArrayList<>(); + + airportData.add("Airport test airport"); + airportData.add("The code of airport is tas"); + airportData.add("Location of airport test airport is 41,2665 12,5994"); + documentData.put("airport", airportData); + + airportCityData.add("Airport is located in the city TestCity"); + airportCityData.add("Country of this city is TestCountry"); + documentData.put("airportCity", airportCityData); - ClassLoader classLoader = getClass().getClassLoader(); - - try(InputStream inputStream = classLoader.getResourceAsStream("sample.pdf")){ - byte[] bytes = new byte[inputStream.available()]; - inputStream.read(bytes); - ByteArrayResource resource = new ByteArrayResource(bytes); - var response = mockMvc.perform(get("/api/reports/airport/1")) - .andExpect(status().isOk()) - .andReturn().getResponse().getContentAsByteArray(); - assertThat(Arrays.equals(response, resource.getByteArray())); - log.debug("response: {}", response); - } catch (IOException e) { - throw new RuntimeException(e); - } + var document = reportDocumentService.reportDocument(documentData, "airport test", ReportType.AIRPORT); + + assertThat(document.toString()).contains("Type/Pages/Count 2"); + assertThat(document.toString()).contains("Airport"); + assertThat(document.toString()).contains("Airport city"); + assertThat(document.toString()).contains("airport test"); + + log.debug("Document: {}", document); } @Test - void getReportAirplaneByIdTest() throws Exception { - log.debug("getReportFlightByIdTest() running"); + void getReportAirplaneByIdTest(){ - ClassLoader classLoader = getClass().getClassLoader(); - - try(InputStream inputStream = classLoader.getResourceAsStream("sample.pdf")){ - byte[] bytes = new byte[inputStream.available()]; - inputStream.read(bytes); - ByteArrayResource resource = new ByteArrayResource(bytes); - var response = mockMvc.perform(get("/api/reports/airplane/1")) - .andExpect(status().isOk()) - .andReturn().getResponse().getContentAsByteArray(); - assertThat(Arrays.equals(response, resource.getByteArray())); - log.debug("response: {}", response); - } catch (IOException e) { - throw new RuntimeException(e); - } + log.debug("getReportAirplaneByIdTest() running"); + List<String> airplaneData = new ArrayList<>(); + List<String> airplaneTypeData = new ArrayList<>(); + airplaneData.add("Airplane name: Test plane"); + airplaneData.add("Airplane capacity: 150"); + documentData.put("airplane", airplaneData); + + airplaneTypeData.add("Airplane type name: Test type"); + documentData.put("airplaneType", airplaneTypeData); + + var document = reportDocumentService.reportDocument(documentData, "airplane test", ReportType.AIRPLANE); + + assertThat(document.toString()).contains("Type/Pages/Count 2"); + assertThat(document.toString()).contains("Airplane"); + assertThat(document.toString()).contains("Airplane type"); + assertThat(document.toString()).contains("airplane test"); + + log.debug("Document: {}", document); } + + + } diff --git a/authorization-client/mvnw b/user-client/mvnw similarity index 100% rename from authorization-client/mvnw rename to user-client/mvnw diff --git a/authorization-client/mvnw.cmd b/user-client/mvnw.cmd similarity index 100% rename from authorization-client/mvnw.cmd rename to user-client/mvnw.cmd diff --git a/authorization-client/pom.xml b/user-client/pom.xml similarity index 77% rename from authorization-client/pom.xml rename to user-client/pom.xml index 4a56609f619917d38f6329a046862d9ba70f74ee..271d8348da70c1212f612c78cf8438d94aed7350 100644 --- a/authorization-client/pom.xml +++ b/user-client/pom.xml @@ -9,12 +9,12 @@ <version>1.0-SNAPSHOT</version> </parent> - <artifactId>authorization-client</artifactId> + <artifactId>user-client</artifactId> <version>1.0-SNAPSHOT</version> - <name>authorization-client</name> + <name>user-client</name> <build> - <finalName>authorization-client-lib</finalName> + <finalName>user-client-lib</finalName> <defaultGoal>install</defaultGoal> <plugins> @@ -28,11 +28,11 @@ </goals> <configuration> <!-- see https://github.com/OpenAPITools/openapi-generator/blob/master/modules/openapi-generator-maven-plugin/README.md --> - <inputSpec>${project.basedir}/../authorization/openapi.yaml</inputSpec> + <inputSpec>${project.basedir}/../user/openapi.yaml</inputSpec> <generatorName>java</generatorName> - <apiPackage>cz.muni.fi.pa165.authorization.client</apiPackage> - <modelPackage>cz.muni.fi.pa165.authorization.client.model</modelPackage> - <invokerPackage>cz.muni.chat.fi.pa165.authorization.client.invoker</invokerPackage> + <apiPackage>cz.muni.fi.pa165.user.client</apiPackage> + <modelPackage>cz.muni.fi.pa165.user.client.model</modelPackage> + <invokerPackage>cz.muni.chat.fi.pa165.user.client.invoker</invokerPackage> <verbose>false</verbose> <generateApiTests>false</generateApiTests> <generateModelTests>false</generateModelTests> @@ -89,5 +89,17 @@ <groupId>javax.annotation</groupId> <artifactId>javax.annotation-api</artifactId> </dependency> + <dependency> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-starter-oauth2-resource-server</artifactId> + </dependency> + <dependency> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-starter-oauth2-client</artifactId> + </dependency> + <dependency> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-starter-web</artifactId> + </dependency> </dependencies> </project> diff --git a/user-client/src/main/java/cz/muni/fi/pa165/user/client/Authorities.java b/user-client/src/main/java/cz/muni/fi/pa165/user/client/Authorities.java new file mode 100644 index 0000000000000000000000000000000000000000..290d039e48ffb1c2679483dbd5b5cc20f0eb11b2 --- /dev/null +++ b/user-client/src/main/java/cz/muni/fi/pa165/user/client/Authorities.java @@ -0,0 +1,7 @@ +package cz.muni.fi.pa165.user.client; + +public class Authorities { + public static final String MANAGER = "SCOPE_test_1"; + public static final String ADMINISTRATOR = "SCOPE_test_2"; + public static final String AUDITOR = "SCOPE_test_3"; +} \ No newline at end of file diff --git a/user-client/src/main/java/cz/muni/fi/pa165/user/client/UserServiceInterceptionConfigurer.java b/user-client/src/main/java/cz/muni/fi/pa165/user/client/UserServiceInterceptionConfigurer.java new file mode 100644 index 0000000000000000000000000000000000000000..604c869f94a07c203653930249eaab45f962e537 --- /dev/null +++ b/user-client/src/main/java/cz/muni/fi/pa165/user/client/UserServiceInterceptionConfigurer.java @@ -0,0 +1,25 @@ +package cz.muni.fi.pa165.user.client; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.config.annotation.InterceptorRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +@Configuration +@ComponentScan +public class UserServiceInterceptionConfigurer implements WebMvcConfigurer { + + private final UserServiceRequestInterceptor interceptor; + + @Autowired + public UserServiceInterceptionConfigurer(UserServiceRequestInterceptor interceptor) { + this.interceptor = interceptor; + } + + @Override + public void addInterceptors(InterceptorRegistry registry) { + // Add the user interceptor to intercept all requests to your REST endpoints + registry.addInterceptor(interceptor).addPathPatterns("/api/**"); + } +} diff --git a/user-client/src/main/java/cz/muni/fi/pa165/user/client/UserServiceRequestInterceptor.java b/user-client/src/main/java/cz/muni/fi/pa165/user/client/UserServiceRequestInterceptor.java new file mode 100644 index 0000000000000000000000000000000000000000..8caeabae9083dc20f060b8bab61ffbe853eef2e7 --- /dev/null +++ b/user-client/src/main/java/cz/muni/fi/pa165/user/client/UserServiceRequestInterceptor.java @@ -0,0 +1,55 @@ +package cz.muni.fi.pa165.user.client; + +import cz.muni.chat.fi.pa165.user.client.invoker.ApiClient; +import cz.muni.chat.fi.pa165.user.client.invoker.ApiException; + +import cz.muni.fi.pa165.user.client.model.NewActionDto; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.oauth2.core.OAuth2AccessToken; +import org.springframework.security.oauth2.server.resource.authentication.BearerTokenAuthentication; +import org.springframework.security.oauth2.server.resource.introspection.OAuth2IntrospectionAuthenticatedPrincipal; +import org.springframework.stereotype.Component; +import org.springframework.web.servlet.HandlerInterceptor; + + +@Component +public class UserServiceRequestInterceptor implements HandlerInterceptor { + + public void sendRequest(HttpServletRequest request) { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + if (authentication == null) { + return; + } + var authPrincipal = (OAuth2IntrospectionAuthenticatedPrincipal) authentication.getPrincipal(); + var token = (OAuth2AccessToken) (((BearerTokenAuthentication) authentication).getToken()); + + // would lead to infinite recursion + if (request.getRequestURI().equals("/api/users/action")) { + return; + } + + try { + var client = new ApiClient(); + client.setRequestInterceptor((builder -> { + builder.header("Authorization", "Bearer " + token.getTokenValue()); + })); + var api = new UserApi(client); + api.registerUserAction(new NewActionDto() + .url(request.getRequestURI()) + .httpMethod(request.getMethod()) + ); + request.getRequestURI(); + } catch (ApiException e) { + e.printStackTrace(); + } + } + + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { + sendRequest(request); + return true; + } +} \ No newline at end of file diff --git a/authorization/.mvn/wrapper/maven-wrapper.jar b/user/.mvn/wrapper/maven-wrapper.jar similarity index 100% rename from authorization/.mvn/wrapper/maven-wrapper.jar rename to user/.mvn/wrapper/maven-wrapper.jar diff --git a/authorization/.mvn/wrapper/maven-wrapper.properties b/user/.mvn/wrapper/maven-wrapper.properties similarity index 100% rename from authorization/.mvn/wrapper/maven-wrapper.properties rename to user/.mvn/wrapper/maven-wrapper.properties diff --git a/user/Dockerfile b/user/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..d131222e38617e1c16b7e0e8e265b68f253b1849 --- /dev/null +++ b/user/Dockerfile @@ -0,0 +1,3 @@ +FROM eclipse-temurin:17-jdk-alpine +COPY target/*.jar /app.jar +ENTRYPOINT ["java", "-jar", "/app.jar"] \ No newline at end of file diff --git a/authorization/mvnw b/user/mvnw similarity index 100% rename from authorization/mvnw rename to user/mvnw diff --git a/authorization/mvnw.cmd b/user/mvnw.cmd similarity index 100% rename from authorization/mvnw.cmd rename to user/mvnw.cmd diff --git a/user/openapi.yaml b/user/openapi.yaml new file mode 100644 index 0000000000000000000000000000000000000000..93c53413d16f1a5cb14e63c9c8233716e22fca41 --- /dev/null +++ b/user/openapi.yaml @@ -0,0 +1,196 @@ +openapi: 3.0.3 +info: + title: Airport Manager uesr microservice + description: Airport Manager user microservice + version: 1.0.0 +servers: + - url: "{scheme}://{server}:{port}" + description: my server + variables: + scheme: + default: http + enum: + - http + - https + server: + default: localhost + port: + default: "8083" +security: + - BearerAuth: [] +paths: + /api/users/: + get: + tags: + - User + summary: List all system users + description: Get an array of all system users + operationId: getAllUsers + responses: + "200": + description: OK + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/UserDto' + /api/users/{id}: + get: + tags: + - User + summary: Get user by id. + description: Returns an object representing an user. + operationId: getUserById + parameters: + - name: id + in: path + required: true + schema: + type: integer + format: int64 + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/UserDto' + + /api/users/{id}/actions: + get: + tags: + - User + summary: Get actions of a user. + description: Returns an actions of the user. + operationId: getUserActions + parameters: + - name: id + in: path + required: true + schema: + type: integer + format: int64 + responses: + "200": + description: OK + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/ActionDto' + + /api/users/action: + post: + tags: + - User + summary: Record action for a user. + description: Record action for a user. + operationId: registerUserAction + requestBody: + description: Data of the action + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/NewActionDto' + responses: + "200": + description: OK + "404": + description: Not Found + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorMessage' + "400": + description: Invalid input + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorMessage' +components: + securitySchemes: + BearerAuth: + type: http + description: "OAuth2 Resource Server, provide a valid access token" + scheme: bearer + schemas: + DomainEntity: + title: Domain Entity + description: Represents a Domain Entity + type: object + required: + - id + properties: + id: + type: integer + description: unique id + format: int64 + example: 1 + discriminator: + propertyName: objectType + + ErrorMessage: + title: Error Message + description: Response body for HTML statuses. + type: object + properties: + message: + type: string + description: reason for error + example: entity not found + + UserDto: + allOf: + - $ref: '#/components/schemas/DomainEntity' + type: object + title: User + description: Represents a system user. + required: + - externalIdentifier + properties: + externalIdentifier: + type: string + description: identfifier of the user + example: 492778@muni.cz + + NewActionDto: + type: object + title: User + description: Represents a user. + required: + - url + - httpMethod + properties: + url: + type: string + description: the url of the method + example: /api/weatherForecast + httpMethod: + type: string + description: the method the user called + example: GET + + ActionDto: + type: object + title: User + description: Represents a user. + required: + - url + - httpMethod + - timestamp + properties: + url: + type: string + description: the url of the method + example: /api/weatherForecast + httpMethod: + type: string + description: the method the user called + example: GET + timestamp: + type: string + format: date-time + description: the method the user called \ No newline at end of file diff --git a/authorization/pom.xml b/user/pom.xml similarity index 76% rename from authorization/pom.xml rename to user/pom.xml index 6313a119d4b1c680eb2a35e2b2f95333fcd2301d..8e8fe61b130aeda815a3b7b5e182bf44e822ea5f 100644 --- a/authorization/pom.xml +++ b/user/pom.xml @@ -8,10 +8,10 @@ <version>1.0-SNAPSHOT</version> </parent> - <artifactId>authorization</artifactId> + <artifactId>user</artifactId> - <name>authorization</name> - <description>Authorization service for Airport Manager</description> + <name>user</name> + <description>User service for Airport Manager</description> <dependencies> @@ -21,6 +21,18 @@ <scope>runtime</scope> </dependency> + <!-- Actuator --> + <dependency> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-starter-actuator</artifactId> + </dependency> + + <!-- Prometheus --> + <dependency> + <groupId>io.micrometer</groupId> + <artifactId>micrometer-registry-prometheus</artifactId> + </dependency> + <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> @@ -41,11 +53,6 @@ <artifactId>jakarta.annotation-api</artifactId> </dependency> - <dependency> - <groupId>jakarta.validation</groupId> - <artifactId>jakarta.validation-api</artifactId> - </dependency> - <dependency> <groupId>io.swagger.core.v3</groupId> <artifactId>swagger-models-jakarta</artifactId> @@ -62,15 +69,25 @@ </dependency> <dependency> - <groupId>javax.validation</groupId> - <artifactId>validation-api</artifactId> + <groupId>org.springdoc</groupId> + <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId> </dependency> <dependency> - <groupId>org.springdoc</groupId> - <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-starter-oauth2-resource-server</artifactId> + </dependency> + + <dependency> + <groupId>cz.muni.fi.pa165</groupId> + <artifactId>user-client</artifactId> + <version>1.0-SNAPSHOT</version> </dependency> + <dependency> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-starter-oauth2-client</artifactId> + </dependency> <!-- for pagination from JPA without actually using JPA --> <dependency> <groupId>org.springframework.data</groupId> @@ -98,12 +115,12 @@ <configuration> <inputSpec>${project.basedir}/openapi.yaml</inputSpec> <generatorName>spring</generatorName> - <apiPackage>cz.muni.fi.pa165.authorization.server.api</apiPackage> - <modelPackage>cz.muni.fi.pa165.authorization.server.model</modelPackage> + <apiPackage>cz.muni.fi.pa165.user.server.api</apiPackage> + <modelPackage>cz.muni.fi.pa165.user.server.model</modelPackage> <!-- https://openapi-generator.tech/docs/generators/spring --> <configOptions> - <basePackage>cz.muni.fi.pa165.authorization.server</basePackage> - <configPackage>cz.muni.fi.pa165.authorization.server.config</configPackage> + <basePackage>cz.muni.fi.pa165.user.server</basePackage> + <configPackage>cz.muni.fi.pa165.user.server.config</configPackage> <useSpringBoot3>true</useSpringBoot3> <useTags>true</useTags> <delegatePattern>true</delegatePattern> diff --git a/user/src/main/java/cz/muni/fi/pa165/user/server/config/AppConfig.java b/user/src/main/java/cz/muni/fi/pa165/user/server/config/AppConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..8b35b95f1dd7cd80b653e54ae2c1eaae42a977dc --- /dev/null +++ b/user/src/main/java/cz/muni/fi/pa165/user/server/config/AppConfig.java @@ -0,0 +1,39 @@ +package cz.muni.fi.pa165.user.server.config; + +import cz.muni.fi.pa165.user.client.Authorities; +import cz.muni.fi.pa165.user.client.UserServiceInterceptionConfigurer; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.http.HttpMethod; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configurers.oauth2.server.resource.OAuth2ResourceServerConfigurer; +import org.springframework.security.web.SecurityFilterChain; + +@Configuration +@Import(UserServiceInterceptionConfigurer.class) +public class AppConfig { + + @Bean + SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { + http.authorizeHttpRequests(x -> x + // swagger: + .requestMatchers("/swagger-ui/**").permitAll() + .requestMatchers("/v3/api-docs/**").permitAll() + .requestMatchers(HttpMethod.GET, "/").permitAll() + .requestMatchers(HttpMethod.GET, "/swagger-ui.html").permitAll() + + // used under the authority of the user using the original api + .requestMatchers("/api/users/action").authenticated() + + // require audit authority for reading from this service + .requestMatchers(HttpMethod.GET, "/api/users").hasAuthority(Authorities.AUDITOR) + .requestMatchers(HttpMethod.GET, "/api/users/**").hasAuthority(Authorities.AUDITOR) + + // default deny + .anyRequest().denyAll() + ) + .oauth2ResourceServer(OAuth2ResourceServerConfigurer::opaqueToken); + return http.build(); + } +} diff --git a/user/src/main/java/cz/muni/fi/pa165/user/server/data/domain/Action.java b/user/src/main/java/cz/muni/fi/pa165/user/server/data/domain/Action.java new file mode 100644 index 0000000000000000000000000000000000000000..6d88eaa18d811ff11a9236cebbacabd0e79fc88d --- /dev/null +++ b/user/src/main/java/cz/muni/fi/pa165/user/server/data/domain/Action.java @@ -0,0 +1,32 @@ +package cz.muni.fi.pa165.user.server.data.domain; + +import jakarta.persistence.*; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.hibernate.annotations.CreationTimestamp; + +import java.time.OffsetDateTime; + +@Data +@NoArgsConstructor +@Entity(name = "actions") +public class Action { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id") + private User user; + + @Column + private String url; + + @Column + private String httpMethod; + + @CreationTimestamp + @Temporal(TemporalType.TIMESTAMP) + private OffsetDateTime timestamp; +} diff --git a/user/src/main/java/cz/muni/fi/pa165/user/server/data/domain/User.java b/user/src/main/java/cz/muni/fi/pa165/user/server/data/domain/User.java new file mode 100644 index 0000000000000000000000000000000000000000..1a91f2e32ab52380e15c82e74a3da6f42879b44e --- /dev/null +++ b/user/src/main/java/cz/muni/fi/pa165/user/server/data/domain/User.java @@ -0,0 +1,31 @@ +package cz.muni.fi.pa165.user.server.data.domain; + +import jakarta.persistence.*; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.hibernate.annotations.CreationTimestamp; +import org.hibernate.annotations.UpdateTimestamp; + +import java.time.OffsetDateTime; +import java.util.List; + +// name is important - "user" and "system_user" are reserved and produce weird errors +@Entity(name = "users") +@Data +@NoArgsConstructor +public class User { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @OneToMany(fetch = FetchType.LAZY, mappedBy = "user") + private List<Action> actions; + + public void addAction(Action action) { + actions.add(action); + } + + @Column(unique = true) + private String externalIdentifier; +} diff --git a/user/src/main/java/cz/muni/fi/pa165/user/server/data/repository/ActionRepository.java b/user/src/main/java/cz/muni/fi/pa165/user/server/data/repository/ActionRepository.java new file mode 100644 index 0000000000000000000000000000000000000000..e0d6ea0325cfe3ae4468878cf292383628ca18e7 --- /dev/null +++ b/user/src/main/java/cz/muni/fi/pa165/user/server/data/repository/ActionRepository.java @@ -0,0 +1,10 @@ +package cz.muni.fi.pa165.user.server.data.repository; + +import cz.muni.fi.pa165.user.server.data.domain.Action; +import org.springframework.data.repository.CrudRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface ActionRepository extends CrudRepository<Action, Long> { + +} diff --git a/user/src/main/java/cz/muni/fi/pa165/user/server/data/repository/UserRepository.java b/user/src/main/java/cz/muni/fi/pa165/user/server/data/repository/UserRepository.java new file mode 100644 index 0000000000000000000000000000000000000000..063d55fabadbd8ca643bd8591f439e6fdb1ecfed --- /dev/null +++ b/user/src/main/java/cz/muni/fi/pa165/user/server/data/repository/UserRepository.java @@ -0,0 +1,16 @@ +package cz.muni.fi.pa165.user.server.data.repository; + +import cz.muni.fi.pa165.user.server.data.domain.User; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.CrudRepository; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; + +import java.util.Optional; + +@Repository +public interface UserRepository extends CrudRepository<User, Long> { + + @Query("SELECT u FROM users u WHERE u.externalIdentifier = :externalIdentifier") + Optional<User> findByExternalIndetifier(@Param("externalIdentifier") String externalIdentifier); +} diff --git a/user/src/main/java/cz/muni/fi/pa165/user/server/facade/UserFacade.java b/user/src/main/java/cz/muni/fi/pa165/user/server/facade/UserFacade.java new file mode 100644 index 0000000000000000000000000000000000000000..8a12fcfbd9264d87d338986ae559d069163fd626 --- /dev/null +++ b/user/src/main/java/cz/muni/fi/pa165/user/server/facade/UserFacade.java @@ -0,0 +1,48 @@ +package cz.muni.fi.pa165.user.server.facade; + +import cz.muni.fi.pa165.user.server.mapper.ActionMapper; +import cz.muni.fi.pa165.user.server.mapper.UserMapper; +import cz.muni.fi.pa165.user.server.model.ActionDto; +import cz.muni.fi.pa165.user.server.model.UserDto; +import cz.muni.fi.pa165.user.server.service.UserService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.List; +import java.util.Optional; +import java.util.stream.StreamSupport; + +@Service +public class UserFacade { + + private final UserService userService; + private final UserMapper userMapper; + private ActionMapper actionMapper; + + @Autowired + public UserFacade(UserService userService, UserMapper userMapper, ActionMapper actionMapper) { + this.userService = userService; + this.userMapper = userMapper; + this.actionMapper = actionMapper; + } + + public Optional<UserDto> findById(Long id) { + return userService.findById(id).map(userMapper::toDto); + } + + public List<UserDto> getAllUsers() { + return StreamSupport.stream(userService.getAllUsers().spliterator(), false) + .map(userMapper::toDto) + .toList(); + } + + public void registerUserAction(String foreignIdentifier, String url, String httpMethod) { + userService.registerUserAction(foreignIdentifier, url, httpMethod); + } + + public List<ActionDto> getActionsOfUser(Long id) { + return StreamSupport.stream(userService.getActionsOfUser(id).spliterator(), false) + .map(actionMapper::toDto) + .toList(); + } +} diff --git a/user/src/main/java/cz/muni/fi/pa165/user/server/mapper/ActionMapper.java b/user/src/main/java/cz/muni/fi/pa165/user/server/mapper/ActionMapper.java new file mode 100644 index 0000000000000000000000000000000000000000..d9f17c5b86774337f59a3ac46c7e753b8cb32291 --- /dev/null +++ b/user/src/main/java/cz/muni/fi/pa165/user/server/mapper/ActionMapper.java @@ -0,0 +1,12 @@ +package cz.muni.fi.pa165.user.server.mapper; + +import cz.muni.fi.pa165.user.server.data.domain.Action; +import cz.muni.fi.pa165.user.server.model.ActionDto; +import org.mapstruct.Mapper; + +@Mapper(componentModel = "spring") +public interface ActionMapper { + Action toEntity(ActionDto actionDto); + + ActionDto toDto(Action user); +} \ No newline at end of file diff --git a/user/src/main/java/cz/muni/fi/pa165/user/server/mapper/UserMapper.java b/user/src/main/java/cz/muni/fi/pa165/user/server/mapper/UserMapper.java new file mode 100644 index 0000000000000000000000000000000000000000..5c02af85c78c87024d6172ba51c3f20af0787b9e --- /dev/null +++ b/user/src/main/java/cz/muni/fi/pa165/user/server/mapper/UserMapper.java @@ -0,0 +1,12 @@ +package cz.muni.fi.pa165.user.server.mapper; + +import cz.muni.fi.pa165.user.server.data.domain.User; +import cz.muni.fi.pa165.user.server.model.UserDto; +import org.mapstruct.Mapper; + +@Mapper(componentModel = "spring") +public interface UserMapper { + User toEntity(UserDto userDto); + + UserDto toDto(User user); +} \ No newline at end of file diff --git a/user/src/main/java/cz/muni/fi/pa165/user/server/rest/UserController.java b/user/src/main/java/cz/muni/fi/pa165/user/server/rest/UserController.java new file mode 100644 index 0000000000000000000000000000000000000000..b4ac4a8ef9409d72326060b29d4de606123decff --- /dev/null +++ b/user/src/main/java/cz/muni/fi/pa165/user/server/rest/UserController.java @@ -0,0 +1,51 @@ +package cz.muni.fi.pa165.user.server.rest; + +import cz.muni.fi.pa165.user.server.api.UserApiDelegate; +import cz.muni.fi.pa165.user.server.facade.UserFacade; +import cz.muni.fi.pa165.user.server.model.ActionDto; +import cz.muni.fi.pa165.user.server.model.NewActionDto; +import cz.muni.fi.pa165.user.server.model.UserDto; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.oauth2.server.resource.introspection.OAuth2IntrospectionAuthenticatedPrincipal; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; + +@RestController +public class UserController implements UserApiDelegate { + + private final UserFacade userFacade; + + @Autowired + public UserController(UserFacade userFacade) { + this.userFacade = userFacade; + } + + @Override + public ResponseEntity<UserDto> getUserById(Long id) { + return ResponseEntity.of(userFacade.findById(id)); + } + + @Override + public ResponseEntity<List<UserDto>> getAllUsers() { + return ResponseEntity.ok(userFacade.getAllUsers()); + } + + @Override + public ResponseEntity<Void> registerUserAction(NewActionDto newActivityDto) { + var principal = (OAuth2IntrospectionAuthenticatedPrincipal) SecurityContextHolder.getContext().getAuthentication().getPrincipal(); + userFacade.registerUserAction( + principal.getAttribute("sub"), + newActivityDto.getUrl(), + newActivityDto.getHttpMethod() + ); + return ResponseEntity.ok().build(); + } + + @Override + public ResponseEntity<List<ActionDto>> getUserActions(Long id) { + return ResponseEntity.ok(userFacade.getActionsOfUser(id)); + } +} diff --git a/user/src/main/java/cz/muni/fi/pa165/user/server/service/UserService.java b/user/src/main/java/cz/muni/fi/pa165/user/server/service/UserService.java new file mode 100644 index 0000000000000000000000000000000000000000..7e9e86469728ede0099f6007bea93fffbe84a717 --- /dev/null +++ b/user/src/main/java/cz/muni/fi/pa165/user/server/service/UserService.java @@ -0,0 +1,53 @@ +package cz.muni.fi.pa165.user.server.service; + +import cz.muni.fi.pa165.user.server.data.domain.Action; +import cz.muni.fi.pa165.user.server.data.domain.User; +import cz.muni.fi.pa165.user.server.data.repository.ActionRepository; +import cz.muni.fi.pa165.user.server.data.repository.UserRepository; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.Collections; +import java.util.Optional; + +import static com.fasterxml.jackson.databind.type.LogicalType.Collection; + +@Service +public class UserService { + + private final UserRepository userRepository; + private final ActionRepository actionRepository; + + @Autowired + public UserService(UserRepository userRepository, ActionRepository actionRepository) { + this.userRepository = userRepository; + this.actionRepository = actionRepository; + } + + public Optional<User> findById(Long id) { + return userRepository.findById(id); + } + + public Iterable<User> getAllUsers() { + return userRepository.findAll(); + } + + public void registerUserAction(String externalIdentifier, String url, String httpMethod) { + Optional<User> maybeUser = userRepository.findByExternalIndetifier(externalIdentifier); + User user = maybeUser.orElseGet(() -> { + var newUser = new User(); + newUser.setExternalIdentifier(externalIdentifier); + newUser = userRepository.save(newUser); + return newUser; + }); + var action = new Action(); + action.setUrl(url); + action.setHttpMethod(httpMethod); + action.setUser(user); + actionRepository.save(action); + } + + public Iterable<Action> getActionsOfUser(Long id) { + return userRepository.findById(id).map(User::getActions).orElse(Collections.EMPTY_LIST); + } +} diff --git a/user/src/main/resources/application.yml b/user/src/main/resources/application.yml new file mode 100644 index 0000000000000000000000000000000000000000..76d917933a8b4fc49d17cb7af49948cfea1b986f --- /dev/null +++ b/user/src/main/resources/application.yml @@ -0,0 +1,38 @@ +spring: + datasource: + url: jdbc:h2:mem:exampleDb + username: sa + password: password + driverClassName: org.h2.Driver + jpa: + database-platform: org.hibernate.dialect.H2Dialect + security: + oauth2: + resourceserver: + opaque-token: + introspection-uri: https://oidc.muni.cz/oidc/introspect + # Martin Kuba's testing resource server + client-id: d57b3a8f-156e-46de-9f27-39c4daee05e1 + client-secret: fa228ebc-4d54-4cda-901e-4d6287f8b1652a9c9c44-73c9-4502-973f-bcdb4a8ec96a +server: + port: 8083 +management: + endpoints: + web: + exposure: + include: 'info,health,metrics,prometheus' + info: + env: + enabled: true + endpoint: + health: + show-details: always + show-components: always + probes: + enabled: true +info: + app: + encoding: 'UTF-8' + java: + source: '17' + target: '17' diff --git a/weather/Dockerfile b/weather/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..d131222e38617e1c16b7e0e8e265b68f253b1849 --- /dev/null +++ b/weather/Dockerfile @@ -0,0 +1,3 @@ +FROM eclipse-temurin:17-jdk-alpine +COPY target/*.jar /app.jar +ENTRYPOINT ["java", "-jar", "/app.jar"] \ No newline at end of file diff --git a/weather/openapi.yaml b/weather/openapi.yaml index 458c08ad7f59e878060030fc07240ce579b479f9..d2479d57a50fc705c503ddb7958c609a40205bf0 100644 --- a/weather/openapi.yaml +++ b/weather/openapi.yaml @@ -26,6 +26,8 @@ servers: tags: - name: Weather description: Microservice for weather. +security: + - BearerAuth: [] paths: /api/isSafeToCreateFlight/{departureAirportId}/{arrivalAirportId}: get: @@ -72,7 +74,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/FlightCreationAdvice' + $ref: '#/components/schemas/FlightCreationAdviceDto' "400": description: Input data not correct content: @@ -107,7 +109,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/WeatherForecast' + $ref: '#/components/schemas/WeatherForecastDto' "400": description: Input data not correct content: @@ -115,35 +117,31 @@ paths: schema: $ref: '../core/openapi.yaml#/components/schemas/ErrorMessage' components: + securitySchemes: + BearerAuth: + type: http + description: "OAuth2 Resource Server, provide a valid access token" + scheme: bearer + bearerFormat: "jwt" schemas: - WeatherReason: - title: Weather reason + FlightCreationAdviceDto: + title: FlightCreationAdviceDto description: | - Enumeration of weather reasons, e.g. whether it is ok - to fly or the reason why not - type: string - example: ok-everything - enum: - - ok-everything - - nok-wind - - nok-temperature - - nok-rain - FlightCreationAdvice: - title: Flight creation advice - description: | - Response of '/api/isSafeToCreateFlight' call. + Flight Creation Advice DTO type: object properties: result: type: boolean description: Result whether it's safe to create such a flight reason: - $ref: '#/components/schemas/WeatherReason' - WeatherForecast: - title: Weather forecast + $ref: '#/components/schemas/WeatherReasonDto' + required: + - result + - reason + WeatherForecastDto: + title: WeatherForecastDto description: | - Information about weather forecast including basic attributes, - e.g. wind speed + Weather Forecast DTO type: object properties: temperature: @@ -160,7 +158,26 @@ components: type: number format: double description: Sum of rain for preceding hour in millimeters + example: 0.3 visibility: type: number format: double description: Visibility in kilometers + example: 12 + required: + - temperature + - windSpeed + - rain + - visibility + WeatherReasonDto: + title: WeatherReasonDto + description: | + Weather Reason DTO + type: string + example: ok-everything + enum: + - ok-everything + - nok-temperature + - nok-wind + - nok-rain + - nok-visibility diff --git a/weather/pom.xml b/weather/pom.xml index 52f2201f00a20f7df0173d7e3ca9813d1be89e1d..40f4bb519ff6156fc49375e272d0eb18ce3a7018 100644 --- a/weather/pom.xml +++ b/weather/pom.xml @@ -14,6 +14,18 @@ <description>Weather forecast service for Airport Manager</description> <dependencies> + <dependency> + <groupId>cz.muni.fi.pa165</groupId> + <artifactId>core-client</artifactId> + <version>1.0-SNAPSHOT</version> + </dependency> + + <dependency> + <groupId>org.mockito</groupId> + <artifactId>mockito-core</artifactId> + <version>5.3.1</version> + <scope>test</scope> + </dependency> <dependency> <groupId>com.h2database</groupId> @@ -21,6 +33,18 @@ <scope>runtime</scope> </dependency> + <!-- Actuator --> + <dependency> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-starter-actuator</artifactId> + </dependency> + + <!-- Prometheus --> + <dependency> + <groupId>io.micrometer</groupId> + <artifactId>micrometer-registry-prometheus</artifactId> + </dependency> + <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> @@ -41,11 +65,6 @@ <artifactId>jakarta.annotation-api</artifactId> </dependency> - <dependency> - <groupId>jakarta.validation</groupId> - <artifactId>jakarta.validation-api</artifactId> - </dependency> - <dependency> <groupId>io.swagger.core.v3</groupId> <artifactId>swagger-models-jakarta</artifactId> @@ -62,13 +81,19 @@ </dependency> <dependency> - <groupId>javax.validation</groupId> - <artifactId>validation-api</artifactId> + <groupId>org.springdoc</groupId> + <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId> </dependency> <dependency> - <groupId>org.springdoc</groupId> - <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-starter-oauth2-resource-server</artifactId> + </dependency> + + <dependency> + <groupId>cz.muni.fi.pa165</groupId> + <artifactId>user-client</artifactId> + <version>1.0-SNAPSHOT</version> </dependency> <!-- for pagination from JPA without actually using JPA --> @@ -77,11 +102,16 @@ <artifactId>spring-data-commons</artifactId> </dependency> + <!-- Bean validator --> + <dependency> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-starter-validation</artifactId> + </dependency> + <!-- for testing --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> - <scope>test</scope> </dependency> </dependencies> diff --git a/weather/src/main/java/cz/muni/fi/pa165/weather/server/WeatherController.java b/weather/src/main/java/cz/muni/fi/pa165/weather/server/WeatherController.java deleted file mode 100644 index 867ead5308c16f2829a8922975b082bbe4f7b328..0000000000000000000000000000000000000000 --- a/weather/src/main/java/cz/muni/fi/pa165/weather/server/WeatherController.java +++ /dev/null @@ -1,32 +0,0 @@ -package cz.muni.fi.pa165.weather.server; - -import cz.muni.fi.pa165.weather.server.api.WeatherApiDelegate; -import cz.muni.fi.pa165.weather.server.model.FlightCreationAdvice; -import cz.muni.fi.pa165.weather.server.model.WeatherForecast; -import cz.muni.fi.pa165.weather.server.model.WeatherReason; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.RestController; - -import java.time.OffsetDateTime; - -@RestController -public class WeatherController implements WeatherApiDelegate { - - @Override - public ResponseEntity<WeatherForecast> getWeatherForecast(Double latitude, Double longitude) { - var weatherForecast = new WeatherForecast() - .temperature(3.14) - .windSpeed(15.92) - .rain(0.8) - .visibility(6.7); - return ResponseEntity.ok(weatherForecast); - } - - @Override - public ResponseEntity<FlightCreationAdvice> isSafeToCreateFlight(Long departureAirportId, Long arrivalAirportId, OffsetDateTime departureTime, OffsetDateTime arrivalTime) { - var flightCreationAdvice = new FlightCreationAdvice() - .result(true) - .reason(WeatherReason.OK_EVERYTHING); - return ResponseEntity.ok(flightCreationAdvice); - } -} diff --git a/weather/src/main/java/cz/muni/fi/pa165/weather/server/config/AppConfig.java b/weather/src/main/java/cz/muni/fi/pa165/weather/server/config/AppConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..70e7db1d163bbcc2921763c7d80a0431ca1310e4 --- /dev/null +++ b/weather/src/main/java/cz/muni/fi/pa165/weather/server/config/AppConfig.java @@ -0,0 +1,31 @@ +package cz.muni.fi.pa165.weather.server.config; + +import cz.muni.fi.pa165.user.client.Authorities; +import cz.muni.fi.pa165.user.client.UserServiceInterceptionConfigurer; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.http.HttpMethod; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configurers.oauth2.server.resource.OAuth2ResourceServerConfigurer; +import org.springframework.security.web.SecurityFilterChain; + +@Configuration +@Import(UserServiceInterceptionConfigurer.class) +public class AppConfig { + + @Bean + SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { + http.authorizeHttpRequests(x -> x + // swagger: + .requestMatchers("/swagger-ui/**").permitAll() + .requestMatchers("/v3/api-docs/**").permitAll() + .requestMatchers(HttpMethod.GET, "/").permitAll() + .requestMatchers(HttpMethod.GET, "/swagger-ui.html").permitAll() + // MANAGER has access to everything in this service + .anyRequest().hasAuthority(Authorities.MANAGER) + ) + .oauth2ResourceServer(OAuth2ResourceServerConfigurer::opaqueToken); + return http.build(); + } +} diff --git a/weather/src/main/java/cz/muni/fi/pa165/weather/server/data/FlightCreationAdvice.java b/weather/src/main/java/cz/muni/fi/pa165/weather/server/data/FlightCreationAdvice.java new file mode 100644 index 0000000000000000000000000000000000000000..c9df61d2f13be6b0fc433b45e7efc1adfe840871 --- /dev/null +++ b/weather/src/main/java/cz/muni/fi/pa165/weather/server/data/FlightCreationAdvice.java @@ -0,0 +1,15 @@ +package cz.muni.fi.pa165.weather.server.data; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.Setter; + +@Getter +@Setter +@RequiredArgsConstructor +public class FlightCreationAdvice { + + private boolean result; + + private WeatherReason weatherReason; +} diff --git a/weather/src/main/java/cz/muni/fi/pa165/weather/server/data/HourlyWeatherForecast.java b/weather/src/main/java/cz/muni/fi/pa165/weather/server/data/HourlyWeatherForecast.java new file mode 100644 index 0000000000000000000000000000000000000000..912393b4521ca89a3d4134725db30ae07c62e750 --- /dev/null +++ b/weather/src/main/java/cz/muni/fi/pa165/weather/server/data/HourlyWeatherForecast.java @@ -0,0 +1,43 @@ +package cz.muni.fi.pa165.weather.server.data; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.Setter; +import lombok.ToString; + +import java.util.List; +import java.util.Map; + +/** + * Entity representation of the data from our Weather API we use, + * see: <a href="https://open-meteo.com/">open-meteo</a>. + */ +@Getter +@Setter +@RequiredArgsConstructor +@ToString +public class HourlyWeatherForecast { + + private double latitude; + + private double longitude; + + @JsonProperty("generationtime_ms") + private double generationTimeMs; + + @JsonProperty("utc_offset_seconds") + private long utcOffsetSeconds; + + private String timezone; + + @JsonProperty("timezone_abbreviation") + private String timezoneAbbreviation; + + private double elevation; + + @JsonProperty("hourly_units") + private Map<String, String> hourlyUnits; + + private Map<String, List<Object>> hourly; +} diff --git a/weather/src/main/java/cz/muni/fi/pa165/weather/server/data/WeatherForecast.java b/weather/src/main/java/cz/muni/fi/pa165/weather/server/data/WeatherForecast.java new file mode 100644 index 0000000000000000000000000000000000000000..f8f7da6e5045e76357182852db9fe76e5db3f8d8 --- /dev/null +++ b/weather/src/main/java/cz/muni/fi/pa165/weather/server/data/WeatherForecast.java @@ -0,0 +1,48 @@ +package cz.muni.fi.pa165.weather.server.data; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.Setter; +import org.springframework.stereotype.Component; + +@Component +@Getter +@Setter +@RequiredArgsConstructor +public class WeatherForecast { + + private double temperature; + + private double windSpeed; + + private double rain; + + private double visibility; + + private static final double MIN_SAFE_TEMPERATURE = -40.0; + + private static final double MAX_SAFE_TEMPERATURE = 40.0; + + private static final double MAX_SAFE_WINDSPEED = 75; + + private static final double MAX_SAFE_RAIN = 3.0; + + private static final double MIN_SAFE_VISIBILITY = 1000.0; + + public boolean temperatureInSafeBounds() { + return MIN_SAFE_TEMPERATURE <= temperature && + temperature <= MAX_SAFE_TEMPERATURE; + } + + public boolean windSpeedInSafeBounds() { + return windSpeed <= MAX_SAFE_WINDSPEED; + } + + public boolean rainInSafeBounds() { + return rain <= MAX_SAFE_RAIN; + } + + public boolean visibilityInSafeBounds() { + return MIN_SAFE_VISIBILITY <= visibility; + } +} diff --git a/weather/src/main/java/cz/muni/fi/pa165/weather/server/data/WeatherReason.java b/weather/src/main/java/cz/muni/fi/pa165/weather/server/data/WeatherReason.java new file mode 100644 index 0000000000000000000000000000000000000000..26564c67ed23f90d7b17fef7b970e89ffd480c12 --- /dev/null +++ b/weather/src/main/java/cz/muni/fi/pa165/weather/server/data/WeatherReason.java @@ -0,0 +1,10 @@ +package cz.muni.fi.pa165.weather.server.data; + +public enum WeatherReason { + + OK_EVERYTHING, + NOK_TEMPERATURE, + NOK_WIND, + NOK_RAIN, + NOK_VISIBILITY +} diff --git a/weather/src/main/java/cz/muni/fi/pa165/weather/server/facade/WeatherFacade.java b/weather/src/main/java/cz/muni/fi/pa165/weather/server/facade/WeatherFacade.java new file mode 100644 index 0000000000000000000000000000000000000000..5a176f54d58f85f1aaa2307966e9cd12a4dd5aa1 --- /dev/null +++ b/weather/src/main/java/cz/muni/fi/pa165/weather/server/facade/WeatherFacade.java @@ -0,0 +1,20 @@ +package cz.muni.fi.pa165.weather.server.facade; + +import cz.muni.fi.pa165.weather.server.model.FlightCreationAdviceDto; +import cz.muni.fi.pa165.weather.server.model.WeatherForecastDto; +import org.springframework.stereotype.Service; + +import java.time.OffsetDateTime; + +@Service +public interface WeatherFacade { + + WeatherForecastDto getWeatherForecast(Double latitude, Double longitude); + + FlightCreationAdviceDto isSafeToCreateFlight( + Long departureAirportId, + Long arrivalAirportId, + OffsetDateTime departureTime, + OffsetDateTime arrivalTime + ); +} diff --git a/weather/src/main/java/cz/muni/fi/pa165/weather/server/facade/WeatherFacadeImpl.java b/weather/src/main/java/cz/muni/fi/pa165/weather/server/facade/WeatherFacadeImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..4c0cc770af5d2bc44a21701c42008d06c5cd9ed6 --- /dev/null +++ b/weather/src/main/java/cz/muni/fi/pa165/weather/server/facade/WeatherFacadeImpl.java @@ -0,0 +1,55 @@ +package cz.muni.fi.pa165.weather.server.facade; + +import cz.muni.fi.pa165.weather.server.data.FlightCreationAdvice; +import cz.muni.fi.pa165.weather.server.data.WeatherForecast; +import cz.muni.fi.pa165.weather.server.mapper.FlightCreationAdviceMapper; +import cz.muni.fi.pa165.weather.server.mapper.WeatherForecastMapper; +import cz.muni.fi.pa165.weather.server.model.FlightCreationAdviceDto; +import cz.muni.fi.pa165.weather.server.model.WeatherForecastDto; +import cz.muni.fi.pa165.weather.server.service.WeatherService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.time.OffsetDateTime; + +@Service +public class WeatherFacadeImpl implements WeatherFacade { + + private final WeatherService weatherService; + private final WeatherForecastMapper weatherForecastMapper; + private final FlightCreationAdviceMapper flightCreationAdviceMapper; + + @Autowired + public WeatherFacadeImpl( + WeatherService weatherService, + WeatherForecastMapper weatherForecastMapper, + FlightCreationAdviceMapper flightCreationAdviceMapper + ) { + this.weatherService = weatherService; + this.weatherForecastMapper = weatherForecastMapper; + this.flightCreationAdviceMapper = flightCreationAdviceMapper; + } + + @Override + public WeatherForecastDto getWeatherForecast(Double latitude, Double longitude) { + WeatherForecast weatherForecast = weatherService.getWeatherForecast(latitude, longitude); + return weatherForecastMapper.toDto(weatherForecast); + } + + @Override + public FlightCreationAdviceDto isSafeToCreateFlight( + Long departureAirportId, + Long arrivalAirportId, + OffsetDateTime departureTime, + OffsetDateTime arrivalTime + ) { + FlightCreationAdvice flightCreationAdvice = weatherService + .isSafeToCreateFlight( + departureAirportId, + arrivalAirportId, + departureTime, + arrivalTime + ); + return flightCreationAdviceMapper.toDto(flightCreationAdvice); + } +} diff --git a/weather/src/main/java/cz/muni/fi/pa165/weather/server/mapper/FlightCreationAdviceMapper.java b/weather/src/main/java/cz/muni/fi/pa165/weather/server/mapper/FlightCreationAdviceMapper.java new file mode 100644 index 0000000000000000000000000000000000000000..ac8e77d1ed7c8b2c1fe5f3fb349f0ec15fb8fa5a --- /dev/null +++ b/weather/src/main/java/cz/muni/fi/pa165/weather/server/mapper/FlightCreationAdviceMapper.java @@ -0,0 +1,19 @@ +package cz.muni.fi.pa165.weather.server.mapper; + +import cz.muni.fi.pa165.weather.server.data.FlightCreationAdvice; +import cz.muni.fi.pa165.weather.server.model.FlightCreationAdviceDto; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +@Component +@Mapper(componentModel = "spring") +public abstract class FlightCreationAdviceMapper { + + @Autowired + protected WeatherReasonMapper weatherReasonMapper; + + @Mapping(target = "reason", expression = "java(weatherReasonMapper.toDto(flightCreationAdvice.getWeatherReason()))") + public abstract FlightCreationAdviceDto toDto(FlightCreationAdvice flightCreationAdvice); +} diff --git a/weather/src/main/java/cz/muni/fi/pa165/weather/server/mapper/WeatherForecastMapper.java b/weather/src/main/java/cz/muni/fi/pa165/weather/server/mapper/WeatherForecastMapper.java new file mode 100644 index 0000000000000000000000000000000000000000..a58f996094daa4defa4ea0466284151532993ddd --- /dev/null +++ b/weather/src/main/java/cz/muni/fi/pa165/weather/server/mapper/WeatherForecastMapper.java @@ -0,0 +1,13 @@ +package cz.muni.fi.pa165.weather.server.mapper; + +import cz.muni.fi.pa165.weather.server.data.WeatherForecast; +import cz.muni.fi.pa165.weather.server.model.WeatherForecastDto; +import org.mapstruct.Mapper; +import org.springframework.stereotype.Component; + +@Component +@Mapper(componentModel = "spring") +public interface WeatherForecastMapper { + + WeatherForecastDto toDto(WeatherForecast weatherForecast); +} diff --git a/weather/src/main/java/cz/muni/fi/pa165/weather/server/mapper/WeatherReasonMapper.java b/weather/src/main/java/cz/muni/fi/pa165/weather/server/mapper/WeatherReasonMapper.java new file mode 100644 index 0000000000000000000000000000000000000000..fa28b4d0da57321c544fec9b0de6c4adf515fbc1 --- /dev/null +++ b/weather/src/main/java/cz/muni/fi/pa165/weather/server/mapper/WeatherReasonMapper.java @@ -0,0 +1,13 @@ +package cz.muni.fi.pa165.weather.server.mapper; + +import cz.muni.fi.pa165.weather.server.data.WeatherReason; +import cz.muni.fi.pa165.weather.server.model.WeatherReasonDto; +import org.mapstruct.Mapper; +import org.springframework.stereotype.Component; + +@Component +@Mapper(componentModel = "spring") +public interface WeatherReasonMapper { + + WeatherReasonDto toDto(WeatherReason weatherReason); +} diff --git a/weather/src/main/java/cz/muni/fi/pa165/weather/server/rest/WeatherController.java b/weather/src/main/java/cz/muni/fi/pa165/weather/server/rest/WeatherController.java new file mode 100644 index 0000000000000000000000000000000000000000..636a34224ab0e684842df9b763db7a356a1cbd9e --- /dev/null +++ b/weather/src/main/java/cz/muni/fi/pa165/weather/server/rest/WeatherController.java @@ -0,0 +1,47 @@ +package cz.muni.fi.pa165.weather.server.rest; + +import cz.muni.fi.pa165.weather.server.api.WeatherApiDelegate; +import cz.muni.fi.pa165.weather.server.facade.WeatherFacade; +import cz.muni.fi.pa165.weather.server.model.FlightCreationAdviceDto; +import cz.muni.fi.pa165.weather.server.model.WeatherForecastDto; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.RestController; + +import java.time.OffsetDateTime; + +@RestController +public class WeatherController implements WeatherApiDelegate { + + private final WeatherFacade weatherFacade; + + @Autowired + public WeatherController(WeatherFacade weatherFacade) { + this.weatherFacade = weatherFacade; + } + + @Override + public ResponseEntity<WeatherForecastDto> getWeatherForecast( + Double latitude, + Double longitude + ) { + return ResponseEntity.ok(weatherFacade.getWeatherForecast(latitude, longitude)); + } + + @Override + public ResponseEntity<FlightCreationAdviceDto> isSafeToCreateFlight( + Long departureAirportId, + Long arrivalAirportId, + OffsetDateTime departureTime, + OffsetDateTime arrivalTime + ) { + return ResponseEntity.ok( + weatherFacade.isSafeToCreateFlight( + departureAirportId, + arrivalAirportId, + departureTime, + arrivalTime + ) + ); + } +} diff --git a/weather/src/main/java/cz/muni/fi/pa165/weather/server/service/TimeUtils.java b/weather/src/main/java/cz/muni/fi/pa165/weather/server/service/TimeUtils.java new file mode 100644 index 0000000000000000000000000000000000000000..1f6642a1d515fee29337f2c0df072cdd65af5456 --- /dev/null +++ b/weather/src/main/java/cz/muni/fi/pa165/weather/server/service/TimeUtils.java @@ -0,0 +1,29 @@ +package cz.muni.fi.pa165.weather.server.service; + +import java.time.LocalDateTime; +import java.time.OffsetDateTime; +import java.time.ZoneId; + +/** + * Utility class for work with {@link LocalDateTime}. + */ +class TimeUtils { + + private TimeUtils() { + // Intentionally made private to prevent instantiation. + } + + static LocalDateTime toLocalDateTime(OffsetDateTime offsetDateTime) { + return offsetDateTime + .atZoneSameInstant(ZoneId.of("UTC+02:00")) + .toLocalDateTime(); + } + + static int getActualHour() { + return getHourOf(LocalDateTime.now()); + } + + static int getHourOf(LocalDateTime localDateTime) { + return localDateTime.getHour(); + } +} diff --git a/weather/src/main/java/cz/muni/fi/pa165/weather/server/service/WeatherService.java b/weather/src/main/java/cz/muni/fi/pa165/weather/server/service/WeatherService.java new file mode 100644 index 0000000000000000000000000000000000000000..b93f76db9f23d19ed832ef68715ef090d14044ae --- /dev/null +++ b/weather/src/main/java/cz/muni/fi/pa165/weather/server/service/WeatherService.java @@ -0,0 +1,43 @@ +package cz.muni.fi.pa165.weather.server.service; + +import cz.muni.fi.pa165.weather.server.data.FlightCreationAdvice; +import cz.muni.fi.pa165.weather.server.data.WeatherForecast; +import org.springframework.stereotype.Service; + +import java.time.OffsetDateTime; + +@Service +public interface WeatherService { + + /** + * Get the weather info for the given {@code latitude} + * and {@code longitude} at this moment. + * + * @param latitude latitude + * @param longitude longitude + * @return weather information + */ + WeatherForecast getWeatherForecast( + double latitude, + double longitude + ); + + /** + * Find out whether it's safe to create the flight based + * on {@code departureAirportId}, {@code arrivalAirportId}, + * {@code departureTime} and {@code arrivalTime}. In case of + * that it isn't safe, returns also the reason why not. + * + * @param departureAirportId departure airport's id + * @param arrivalAirportId arrival airport's id + * @param departureTime flight departure time + * @param arrivalTime flight arrival time + * @return result as written above + */ + FlightCreationAdvice isSafeToCreateFlight( + Long departureAirportId, + Long arrivalAirportId, + OffsetDateTime departureTime, + OffsetDateTime arrivalTime + ); +} diff --git a/weather/src/main/java/cz/muni/fi/pa165/weather/server/service/WeatherServiceImpl.java b/weather/src/main/java/cz/muni/fi/pa165/weather/server/service/WeatherServiceImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..187127ec2d8d6697f3ef82a8bbe916d740370260 --- /dev/null +++ b/weather/src/main/java/cz/muni/fi/pa165/weather/server/service/WeatherServiceImpl.java @@ -0,0 +1,188 @@ +package cz.muni.fi.pa165.weather.server.service; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import cz.muni.fi.pa165.core.client.AirportApi; +import cz.muni.fi.pa165.core.client.invoker.ApiClient; +import cz.muni.fi.pa165.core.client.invoker.ApiException; +import cz.muni.fi.pa165.core.client.model.AirportDto; +import cz.muni.fi.pa165.weather.server.data.FlightCreationAdvice; +import cz.muni.fi.pa165.weather.server.data.HourlyWeatherForecast; +import cz.muni.fi.pa165.weather.server.data.WeatherForecast; +import cz.muni.fi.pa165.weather.server.data.WeatherReason; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.oauth2.core.OAuth2AccessToken; +import org.springframework.security.oauth2.server.resource.authentication.BearerTokenAuthentication; +import org.springframework.security.oauth2.server.resource.introspection.OAuth2IntrospectionAuthenticatedPrincipal; +import org.springframework.stereotype.Service; +import org.springframework.web.client.RestTemplate; + +import java.time.OffsetDateTime; + +@Service +public class WeatherServiceImpl implements WeatherService { + + private final RestTemplate restTemplate = new RestTemplate(); + private final ObjectMapper objectMapper = new ObjectMapper(); + + private static final String TEMPERATURE_ATTRIBUTE_NAME = "temperature_2m"; + private static final String RAIN_ATTRIBUTE_NAME = "rain"; + private static final String WIND_ATTRIBUTE_NAME = "windspeed_10m"; + private static final String VISIBILITY_ATTRIBUTE_NAME = "visibility"; + + @Override + public WeatherForecast getWeatherForecast(double latitude, double longitude) { + String weatherForecastJsonAsString = restTemplate.getForObject( + getHourlyForecastUrl(latitude, longitude), + String.class + ); + try { + HourlyWeatherForecast hourlyWeatherForecast = objectMapper + .readValue(weatherForecastJsonAsString, HourlyWeatherForecast.class); + + return getActualWeatherForecastFromHourlyForecast(hourlyWeatherForecast); + } catch (JsonProcessingException ex) { + throw new RuntimeException(ex); + } + } + + @Override + public FlightCreationAdvice isSafeToCreateFlight( + Long departureAirportId, + Long arrivalAirportId, + OffsetDateTime departureTime, + OffsetDateTime arrivalTime + ) { + try { + var authentication = SecurityContextHolder.getContext().getAuthentication(); + var token = (OAuth2AccessToken) (((BearerTokenAuthentication) authentication).getToken()); + var client = new ApiClient(); + client.setRequestInterceptor((builder -> { + builder.header("Authorization", "Bearer " + token.getTokenValue()); + })); + AirportApi airportClient = new AirportApi(client); + AirportDto departureAirportDto = airportClient.getAirportById(departureAirportId); + AirportDto arrivalAirportDto = airportClient.getAirportById(arrivalAirportId); + + WeatherForecast departureForecast = getWeatherForecast( + departureAirportDto.getLocation().getLatitude(), + departureAirportDto.getLocation().getLongitude() + ); + WeatherForecast arrivalForecast = getWeatherForecast( + arrivalAirportDto.getLocation().getLatitude(), + arrivalAirportDto.getLocation().getLongitude() + ); + + var flightCreationAdvice = new FlightCreationAdvice(); + flightCreationAdvice.setResult(false); + if (!departureForecast.temperatureInSafeBounds() + || !arrivalForecast.temperatureInSafeBounds()) { + flightCreationAdvice.setWeatherReason(WeatherReason.NOK_TEMPERATURE); + } else if (!departureForecast.windSpeedInSafeBounds() || + !arrivalForecast.windSpeedInSafeBounds()) { + flightCreationAdvice.setWeatherReason(WeatherReason.NOK_WIND); + } else if (!departureForecast.rainInSafeBounds() || + !arrivalForecast.rainInSafeBounds()) { + flightCreationAdvice.setWeatherReason(WeatherReason.NOK_RAIN); + } else if (!departureForecast.visibilityInSafeBounds() || + !arrivalForecast.visibilityInSafeBounds()) { + flightCreationAdvice.setWeatherReason(WeatherReason.NOK_VISIBILITY); + } else { + flightCreationAdvice.setResult(true); + flightCreationAdvice.setWeatherReason(WeatherReason.OK_EVERYTHING); + } + + return flightCreationAdvice; + } catch (ApiException e) { + throw new RuntimeException(e); + } + } + + private WeatherForecast getActualWeatherForecastFromHourlyForecast(HourlyWeatherForecast hourlyWeatherForecast) { + return getWeatherForecastFromHourlyForHour(hourlyWeatherForecast, TimeUtils.getActualHour()); + } + + WeatherForecast getWeatherForecastFromHourlyForHour( + HourlyWeatherForecast hourlyWeatherForecast, + int actualHour + ) { + var weatherForecast = new WeatherForecast(); + weatherForecast.setTemperature(parseTemperature(hourlyWeatherForecast, actualHour)); + weatherForecast.setRain(parseRain(hourlyWeatherForecast, actualHour)); + weatherForecast.setWindSpeed(parseWindSpeed(hourlyWeatherForecast, actualHour)); + weatherForecast.setVisibility(parseVisibility(hourlyWeatherForecast, actualHour)); + + return weatherForecast; + } + + + private double parseValueByAttributeNameAndHour( + HourlyWeatherForecast hourlyWeatherForecast, + String attributeName, + int actualHour + ) { + return (double) hourlyWeatherForecast + .getHourly() + .get(attributeName) + .get(actualHour); + } + + private double parseTemperature( + HourlyWeatherForecast hourlyWeatherForecast, + int actualHour + ) { + return parseValueByAttributeNameAndHour( + hourlyWeatherForecast, + TEMPERATURE_ATTRIBUTE_NAME, + actualHour + ); + } + + private double parseRain( + HourlyWeatherForecast hourlyWeatherForecast, + int actualHour + ) { + return parseValueByAttributeNameAndHour( + hourlyWeatherForecast, + RAIN_ATTRIBUTE_NAME, + actualHour + ); + } + + private double parseWindSpeed( + HourlyWeatherForecast hourlyWeatherForecast, + int actualHour + ) { + return parseValueByAttributeNameAndHour( + hourlyWeatherForecast, + WIND_ATTRIBUTE_NAME, + actualHour + ); + } + + private double parseVisibility( + HourlyWeatherForecast hourlyWeatherForecast, + int actualHour + ) { + return parseValueByAttributeNameAndHour( + hourlyWeatherForecast, + VISIBILITY_ATTRIBUTE_NAME, + actualHour + ); + } + + static String getHourlyForecastUrl( + double latitude, + double longitude + ) { + return "https://api.open-meteo.com/v1/forecast" + + "?latitude=" + latitude + + "&longitude=" + longitude + + "&hourly=" + + TEMPERATURE_ATTRIBUTE_NAME + "," + + RAIN_ATTRIBUTE_NAME + "," + + VISIBILITY_ATTRIBUTE_NAME + "," + + WIND_ATTRIBUTE_NAME + + "&forecast_days=1"; + } +} diff --git a/weather/src/main/resources/application.yml b/weather/src/main/resources/application.yml index 08729559efdd11fb686be558299337798e337ec7..ac1e77f44374277a01de56cee1e5fc288cbd4aa2 100644 --- a/weather/src/main/resources/application.yml +++ b/weather/src/main/resources/application.yml @@ -6,7 +6,35 @@ spring: driverClassName: org.h2.Driver jpa: database-platform: org.hibernate.dialect.H2Dialect + security: + oauth2: + resourceserver: + opaque-token: + introspection-uri: https://oidc.muni.cz/oidc/introspect + # Martin Kuba's testing resource server + client-id: d57b3a8f-156e-46de-9f27-39c4daee05e1 + client-secret: fa228ebc-4d54-4cda-901e-4d6287f8b1652a9c9c44-73c9-4502-973f-bcdb4a8ec96a # Let's make weather microservice run on port 8088 # for now to not collide with another microservices server: - port: 8088 \ No newline at end of file + port: 8088 +management: + endpoints: + web: + exposure: + include: 'info,health,metrics,prometheus' + info: + env: + enabled: true + endpoint: + health: + show-details: always + show-components: always + probes: + enabled: true +info: + app: + encoding: 'UTF-8' + java: + source: '17' + target: '17' diff --git a/weather/src/test/java/cz/muni/fi/pa165/weather/server/WeatherApplicationIT.java b/weather/src/test/java/cz/muni/fi/pa165/weather/server/WeatherApplicationIT.java index 59dfb1620e3da0b2043a00ac6617d4f6c19c3fbd..2e96bcc410662cc95b3b5393c41e15418e57cb73 100644 --- a/weather/src/test/java/cz/muni/fi/pa165/weather/server/WeatherApplicationIT.java +++ b/weather/src/test/java/cz/muni/fi/pa165/weather/server/WeatherApplicationIT.java @@ -1,18 +1,25 @@ package cz.muni.fi.pa165.weather.server; import com.fasterxml.jackson.databind.ObjectMapper; -import cz.muni.fi.pa165.weather.server.model.FlightCreationAdvice; -import cz.muni.fi.pa165.weather.server.model.WeatherForecast; -import cz.muni.fi.pa165.weather.server.model.WeatherReason; +import cz.muni.fi.pa165.weather.server.facade.WeatherFacade; +import cz.muni.fi.pa165.weather.server.model.FlightCreationAdviceDto; +import cz.muni.fi.pa165.weather.server.model.WeatherForecastDto; +import cz.muni.fi.pa165.weather.server.model.WeatherReasonDto; import org.junit.jupiter.api.Test; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.test.web.servlet.MockMvc; +import java.time.OffsetDateTime; + import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.when; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @@ -21,7 +28,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers. * Integration tests. Run by "mvn verify". */ @SpringBootTest -@AutoConfigureMockMvc +@AutoConfigureMockMvc(addFilters = false) class WeatherApplicationIT { private static final Logger log = LoggerFactory.getLogger(WeatherApplicationIT.class); @@ -32,6 +39,9 @@ class WeatherApplicationIT { @Autowired ObjectMapper objectMapper; + @MockBean + private WeatherFacade weatherFacade; + @Test void getWeatherForecastTest() throws Exception { log.debug("getWeatherForecastTest() running"); @@ -39,7 +49,15 @@ class WeatherApplicationIT { var expectedTemperature = 3.14; var expectedWindSpeed = 15.92; var expectedRain = 0.8; - var expectedVisibility = 6.7; + var expectedVisibility = 6000.7; + + when(weatherFacade.getWeatherForecast(4.0, 4.0)) + .thenReturn(new WeatherForecastDto() + .temperature(expectedTemperature) + .windSpeed(expectedWindSpeed) + .rain(expectedRain) + .visibility(expectedVisibility) + ); var response = mockMvc.perform(get("/api/weatherForecast?latitude=4&longitude=4")) .andExpect(status().isOk()) @@ -50,7 +68,10 @@ class WeatherApplicationIT { .andReturn().getResponse().getContentAsString(); log.debug("response: {}", response); - var weatherForecastResponse = objectMapper.readValue(response, WeatherForecast.class); + verify(weatherFacade).getWeatherForecast(4.0, 4.0); + verifyNoMoreInteractions(weatherFacade); + + var weatherForecastResponse = objectMapper.readValue(response, WeatherForecastDto.class); assertThat(weatherForecastResponse.getTemperature()).isEqualTo(expectedTemperature); assertThat(weatherForecastResponse.getWindSpeed()).isEqualTo(expectedWindSpeed); assertThat(weatherForecastResponse.getRain()).isEqualTo(expectedRain); @@ -62,7 +83,22 @@ class WeatherApplicationIT { log.debug("isSafeToCreateFlightTest() running"); var expectedResult = true; - var expectedReason = WeatherReason.OK_EVERYTHING; + var expectedReason = WeatherReasonDto.OK_EVERYTHING; + + String departureTimeAsString = "2012-12-31T22:00:00.000Z"; + String arrivalTimeAsString = "2012-12-31T23:20:00.000Z"; + OffsetDateTime departureTime = OffsetDateTime.parse(departureTimeAsString); + OffsetDateTime arrivalTime = OffsetDateTime.parse(arrivalTimeAsString); + + when(weatherFacade.isSafeToCreateFlight( + 1L, + 2L, + departureTime, + arrivalTime) + ).thenReturn(new FlightCreationAdviceDto() + .result(expectedResult) + .reason(expectedReason) + ); var response = mockMvc.perform(get("/api/isSafeToCreateFlight/1/2?departureTime=2012-12-31T22:00:00.000Z&arrivalTime=2012-12-31T23:20:00.000Z")) .andExpect(status().isOk()) @@ -71,7 +107,10 @@ class WeatherApplicationIT { .andReturn().getResponse().getContentAsString(); log.debug("response: {}", response); - var weatherForecastResponse = objectMapper.readValue(response, FlightCreationAdvice.class); + verify(weatherFacade).isSafeToCreateFlight(1L, 2L, departureTime, arrivalTime); + verifyNoMoreInteractions(weatherFacade); + + var weatherForecastResponse = objectMapper.readValue(response, FlightCreationAdviceDto.class); assertThat(weatherForecastResponse.getResult()).isEqualTo(expectedResult); assertThat(weatherForecastResponse.getReason()).isEqualTo(expectedReason); } diff --git a/weather/src/test/java/cz/muni/fi/pa165/weather/server/data/WeatherForecastTest.java b/weather/src/test/java/cz/muni/fi/pa165/weather/server/data/WeatherForecastTest.java new file mode 100644 index 0000000000000000000000000000000000000000..c0448f5f7544fded911b000fc5e764ebde4a79d8 --- /dev/null +++ b/weather/src/test/java/cz/muni/fi/pa165/weather/server/data/WeatherForecastTest.java @@ -0,0 +1,77 @@ +package cz.muni.fi.pa165.weather.server.data; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import static org.junit.jupiter.api.Assertions.*; + +@SpringBootTest +class WeatherForecastTest { + + @Autowired + private WeatherForecast weatherForecast; + + @Test + void temperatureInSafeBoundsTrue() { + weatherForecast.setTemperature(21.2); + + assertTrue(weatherForecast.temperatureInSafeBounds()); + } + + @Test + void temperatureTooHigh() { + weatherForecast.setTemperature(42.42); + + assertFalse(weatherForecast.temperatureInSafeBounds()); + } + + @Test + void temperatureTooLow() { + weatherForecast.setTemperature(-42.42); + + assertFalse(weatherForecast.temperatureInSafeBounds()); + } + + @Test + void windSpeedInSafeBoundsTrue() { + weatherForecast.setWindSpeed(13); + + assertTrue(weatherForecast.windSpeedInSafeBounds()); + } + + @Test + void windSpeedTooHigh() { + weatherForecast.setWindSpeed(81); + + assertFalse(weatherForecast.windSpeedInSafeBounds()); + } + + @Test + void rainInSafeBoundsTrue() { + weatherForecast.setRain(1.2); + + assertTrue(weatherForecast.rainInSafeBounds()); + } + + @Test + void rainsTooMuch() { + weatherForecast.setRain(3.2); + + assertFalse(weatherForecast.rainInSafeBounds()); + } + + @Test + void visibilityInSafeBoundsTrue() { + weatherForecast.setVisibility(13500); + + assertTrue(weatherForecast.visibilityInSafeBounds()); + } + + @Test + void visibilityTooLow() { + weatherForecast.setVisibility(911); + + assertFalse(weatherForecast.visibilityInSafeBounds()); + } +} \ No newline at end of file diff --git a/weather/src/test/java/cz/muni/fi/pa165/weather/server/facade/WeatherFacadeTest.java b/weather/src/test/java/cz/muni/fi/pa165/weather/server/facade/WeatherFacadeTest.java new file mode 100644 index 0000000000000000000000000000000000000000..80bd8d8180214e39fb99d860890e835128080e1a --- /dev/null +++ b/weather/src/test/java/cz/muni/fi/pa165/weather/server/facade/WeatherFacadeTest.java @@ -0,0 +1,88 @@ +package cz.muni.fi.pa165.weather.server.facade; + +import cz.muni.fi.pa165.weather.server.data.FlightCreationAdvice; +import cz.muni.fi.pa165.weather.server.data.WeatherForecast; +import cz.muni.fi.pa165.weather.server.data.WeatherReason; +import cz.muni.fi.pa165.weather.server.model.FlightCreationAdviceDto; +import cz.muni.fi.pa165.weather.server.model.WeatherForecastDto; +import cz.muni.fi.pa165.weather.server.model.WeatherReasonDto; +import cz.muni.fi.pa165.weather.server.service.WeatherService; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; + +import java.time.OffsetDateTime; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.mockito.ArgumentMatchers.anyDouble; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.when; + +@SpringBootTest +class WeatherFacadeTest { + + @Autowired + private WeatherFacade weatherFacade; + + @MockBean + private WeatherService weatherService; + + private static final WeatherForecast weatherForecast = new WeatherForecast(); + private static final FlightCreationAdvice flightCreationAdvice = new FlightCreationAdvice(); + + @BeforeEach + void setUp() { + weatherForecast.setTemperature(21.2); + weatherForecast.setWindSpeed(13); + weatherForecast.setRain(1.2); + weatherForecast.setVisibility(13900); + + flightCreationAdvice.setResult(false); + flightCreationAdvice.setWeatherReason(WeatherReason.NOK_WIND); + } + + @Test + void getWeatherForecast() { + when(weatherService.getWeatherForecast(anyDouble(), anyDouble())) + .thenReturn(weatherForecast); + + WeatherForecastDto weatherForecastDto = weatherFacade.getWeatherForecast(42.42, -42.42); + + verify(weatherService) + .getWeatherForecast(42.42, -42.42); + verifyNoMoreInteractions(weatherService); + assertThat(weatherForecastDto.getTemperature()) + .isEqualTo(weatherForecast.getTemperature()); + assertThat(weatherForecastDto.getWindSpeed()) + .isEqualTo(weatherForecast.getWindSpeed()); + assertThat(weatherForecastDto.getRain()) + .isEqualTo(weatherForecast.getRain()); + assertThat(weatherForecastDto.getVisibility()) + .isEqualTo(weatherForecast.getVisibility()); + } + + @Test + void isSafeToCreateFlight() { + var departureTime = OffsetDateTime.parse("2023-04-17T17:42:00Z"); + var arrivalTime = OffsetDateTime.parse("2023-04-17T21:22:00+02:00"); + when(weatherService.isSafeToCreateFlight(1L, 13L, departureTime, arrivalTime)) + .thenReturn(flightCreationAdvice); + + FlightCreationAdviceDto flightCreationAdviceDto = weatherFacade.isSafeToCreateFlight( + 1L, + 13L, + departureTime, + arrivalTime + ); + + verify(weatherService).isSafeToCreateFlight(1L, 13L, departureTime, arrivalTime); + verifyNoMoreInteractions(weatherService); + assertFalse(flightCreationAdviceDto.getResult()); + assertThat(flightCreationAdviceDto.getReason()) + .isEqualTo(WeatherReasonDto.NOK_WIND); + } +} \ No newline at end of file diff --git a/weather/src/test/java/cz/muni/fi/pa165/weather/server/mapper/FlightCreationAdviceMapperTest.java b/weather/src/test/java/cz/muni/fi/pa165/weather/server/mapper/FlightCreationAdviceMapperTest.java new file mode 100644 index 0000000000000000000000000000000000000000..0d48669d0e1d2f27f5449667a008109e48aec3f2 --- /dev/null +++ b/weather/src/test/java/cz/muni/fi/pa165/weather/server/mapper/FlightCreationAdviceMapperTest.java @@ -0,0 +1,32 @@ +package cz.muni.fi.pa165.weather.server.mapper; + +import cz.muni.fi.pa165.weather.server.data.FlightCreationAdvice; +import cz.muni.fi.pa165.weather.server.data.WeatherReason; +import cz.muni.fi.pa165.weather.server.model.FlightCreationAdviceDto; +import cz.muni.fi.pa165.weather.server.model.WeatherReasonDto; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertFalse; + +@SpringBootTest +class FlightCreationAdviceMapperTest { + + @Autowired + private FlightCreationAdviceMapper flightCreationAdviceMapper; + + @Test + void toDto() { + var flightCreationAdviceEntity = new FlightCreationAdvice(); + flightCreationAdviceEntity.setResult(false); + flightCreationAdviceEntity.setWeatherReason(WeatherReason.NOK_RAIN); + + FlightCreationAdviceDto flightCreationAdviceDto = flightCreationAdviceMapper.toDto(flightCreationAdviceEntity); + + assertFalse(flightCreationAdviceDto.getResult()); + assertThat(flightCreationAdviceDto.getReason()) + .isEqualTo(WeatherReasonDto.NOK_RAIN); + } +} \ No newline at end of file diff --git a/weather/src/test/java/cz/muni/fi/pa165/weather/server/mapper/WeatherForecastMapperTest.java b/weather/src/test/java/cz/muni/fi/pa165/weather/server/mapper/WeatherForecastMapperTest.java new file mode 100644 index 0000000000000000000000000000000000000000..28ea90c93bd575a1f26651223889a582dcae1820 --- /dev/null +++ b/weather/src/test/java/cz/muni/fi/pa165/weather/server/mapper/WeatherForecastMapperTest.java @@ -0,0 +1,36 @@ +package cz.muni.fi.pa165.weather.server.mapper; + +import cz.muni.fi.pa165.weather.server.data.WeatherForecast; +import cz.muni.fi.pa165.weather.server.model.WeatherForecastDto; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import static org.assertj.core.api.Assertions.assertThat; + +@SpringBootTest +class WeatherForecastMapperTest { + + @Autowired + private WeatherForecastMapper weatherForecastMapper; + + @Test + void toDto() { + var weatherForecastEntity = new WeatherForecast(); + weatherForecastEntity.setTemperature(23.1); + weatherForecastEntity.setRain(0.3); + weatherForecastEntity.setWindSpeed(13.3); + weatherForecastEntity.setVisibility(18910); + + WeatherForecastDto weatherForecastDto = weatherForecastMapper.toDto(weatherForecastEntity); + + assertThat(weatherForecastDto.getTemperature()) + .isEqualTo(weatherForecastEntity.getTemperature()); + assertThat(weatherForecastDto.getRain()) + .isEqualTo(weatherForecastEntity.getRain()); + assertThat(weatherForecastDto.getWindSpeed()) + .isEqualTo(weatherForecastEntity.getWindSpeed()); + assertThat(weatherForecastDto.getVisibility()) + .isEqualTo(weatherForecastEntity.getVisibility()); + } +} \ No newline at end of file diff --git a/weather/src/test/java/cz/muni/fi/pa165/weather/server/mapper/WeatherReasonMapperTest.java b/weather/src/test/java/cz/muni/fi/pa165/weather/server/mapper/WeatherReasonMapperTest.java new file mode 100644 index 0000000000000000000000000000000000000000..9320207b6a52d7f5b4aed1d06e85d1bd6a2d59cc --- /dev/null +++ b/weather/src/test/java/cz/muni/fi/pa165/weather/server/mapper/WeatherReasonMapperTest.java @@ -0,0 +1,36 @@ +package cz.muni.fi.pa165.weather.server.mapper; + +import cz.muni.fi.pa165.weather.server.data.WeatherReason; +import cz.muni.fi.pa165.weather.server.model.WeatherReasonDto; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import static org.assertj.core.api.Assertions.assertThat; + +@SpringBootTest +class WeatherReasonMapperTest { + + @Autowired + private WeatherReasonMapper weatherReasonMapper; + + @Test + void toDtoWhenOkEverything() { + var weatherReasonEntity = WeatherReason.OK_EVERYTHING; + + WeatherReasonDto weatherReasonDto = weatherReasonMapper.toDto(weatherReasonEntity); + + assertThat(weatherReasonDto) + .isEqualTo(WeatherReasonDto.OK_EVERYTHING); + } + + @Test + void toDtoWhenNokWind() { + var weatherReasonEntity = WeatherReason.NOK_WIND; + + WeatherReasonDto weatherReasonDto = weatherReasonMapper.toDto(weatherReasonEntity); + + assertThat(weatherReasonDto) + .isEqualTo(WeatherReasonDto.NOK_WIND); + } +} \ No newline at end of file diff --git a/weather/src/test/java/cz/muni/fi/pa165/weather/server/service/TimeUtilsTest.java b/weather/src/test/java/cz/muni/fi/pa165/weather/server/service/TimeUtilsTest.java new file mode 100644 index 0000000000000000000000000000000000000000..711106820068c9ecc81622e89859b3380496a19d --- /dev/null +++ b/weather/src/test/java/cz/muni/fi/pa165/weather/server/service/TimeUtilsTest.java @@ -0,0 +1,59 @@ +package cz.muni.fi.pa165.weather.server.service; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +import java.time.LocalDateTime; +import java.time.Month; +import java.time.OffsetDateTime; + +import static org.assertj.core.api.Assertions.assertThat; + +@SpringBootTest +class TimeUtilsTest { + + @Test + void toLocalDateTimeFromUTC() { + OffsetDateTime offsetDateTime = OffsetDateTime + .parse("2023-04-17T17:42:18Z"); + + LocalDateTime localDateTime = TimeUtils.toLocalDateTime(offsetDateTime); + + assertThat(localDateTime) + .isEqualTo(LocalDateTime.of(2023, Month.APRIL, 17, 19, 42, 18)); + } + + @Test + void toLocalDateTimeFromPositiveTimeZone() { + OffsetDateTime offsetDateTime = OffsetDateTime + .parse("2014-09-04T21:18:13+01:00"); + + LocalDateTime localDateTime = TimeUtils.toLocalDateTime(offsetDateTime); + + assertThat(localDateTime) + .isEqualTo(LocalDateTime.of(2014, Month.SEPTEMBER, 4, 22, 18, 13)); + } + + @Test + void toLocalDateTimeFromNegativeZone() { + OffsetDateTime offsetDateTime = OffsetDateTime + .parse("2023-05-05T13:09:42-02:00"); + + LocalDateTime localDateTime = TimeUtils.toLocalDateTime(offsetDateTime); + + assertThat(localDateTime) + .isEqualTo(LocalDateTime.of(2023, Month.MAY, 5, 17, 9, 42)); + } + + @Test + void getHourIndexOf() { + LocalDateTime localDateTime = LocalDateTime.of( + 2023, Month.APRIL, 17, 14, 42 + ); + + int hourIndex = TimeUtils.getHourOf(localDateTime); + + assertThat(hourIndex) + .isEqualTo(14); + } +} \ No newline at end of file diff --git a/weather/src/test/java/cz/muni/fi/pa165/weather/server/service/WeatherServiceImplTest.java b/weather/src/test/java/cz/muni/fi/pa165/weather/server/service/WeatherServiceImplTest.java new file mode 100644 index 0000000000000000000000000000000000000000..cba9a4c300ac0f6359fcb632211f146695440f8c --- /dev/null +++ b/weather/src/test/java/cz/muni/fi/pa165/weather/server/service/WeatherServiceImplTest.java @@ -0,0 +1,70 @@ +package cz.muni.fi.pa165.weather.server.service; + +import cz.muni.fi.pa165.weather.server.data.HourlyWeatherForecast; +import cz.muni.fi.pa165.weather.server.data.WeatherForecast; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import java.util.List; +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; + +@SpringBootTest +class WeatherServiceImplTest { + + private static final HourlyWeatherForecast hourlyWeatherForecast = new HourlyWeatherForecast(); + + @Autowired + private WeatherServiceImpl weatherService; + + @BeforeEach + void setUp() { + hourlyWeatherForecast.setHourly(Map.ofEntries( + Map.entry("temperature_2m", List.of(14.2, -21.3, 12.8)), + Map.entry("rain", List.of(0.9, 0.0, 0.1)), + Map.entry("windspeed_10m", List.of(31.2, 29.1, 42.42)), + Map.entry("visibility", List.of(12800.0, 13200.0, 5500.0)) + )); + } + + @Test + void getWeatherForecastFromHourlyForHour0() { + WeatherForecast weatherForecast = weatherService + .getWeatherForecastFromHourlyForHour(hourlyWeatherForecast, 0); + + assertThat(weatherForecast.getTemperature()) + .isEqualTo(14.2); + assertThat(weatherForecast.getRain()) + .isEqualTo(0.9); + assertThat(weatherForecast.getWindSpeed()) + .isEqualTo(31.2); + assertThat(weatherForecast.getVisibility()) + .isEqualTo(12800.0); + } + + @Test + void getWeatherForecastFromHourlyForHour1() { + WeatherForecast weatherForecast = weatherService + .getWeatherForecastFromHourlyForHour(hourlyWeatherForecast, 1); + + assertThat(weatherForecast.getTemperature()) + .isEqualTo(-21.3); + assertThat(weatherForecast.getRain()) + .isEqualTo(0.0); + assertThat(weatherForecast.getWindSpeed()) + .isEqualTo(29.1); + assertThat(weatherForecast.getVisibility()) + .isEqualTo(13200.0); + } + + @Test + void getHourlyForecastUrl() { + String hourlyForecastUrl = WeatherServiceImpl.getHourlyForecastUrl(42.42, -42.42); + + assertThat(hourlyForecastUrl) + .isEqualTo("https://api.open-meteo.com/v1/forecast?latitude=42.42&longitude=-42.42&hourly=temperature_2m,rain,visibility,windspeed_10m&forecast_days=1"); + } +} \ No newline at end of file