├── .gitattributes ├── .DS_Store ├── src ├── .DS_Store ├── main │ ├── .DS_Store │ ├── java │ │ ├── .DS_Store │ │ └── tr │ │ │ ├── .DS_Store │ │ │ └── com │ │ │ ├── .DS_Store │ │ │ └── rsakin │ │ │ └── taskmanagementapp │ │ │ ├── model │ │ │ ├── dto │ │ │ │ ├── response │ │ │ │ │ ├── AuthResponse.java │ │ │ │ │ ├── TaskStatistics.java │ │ │ │ │ └── TaskResponseDTO.java │ │ │ │ └── request │ │ │ │ │ ├── PriorityUpdateRequest.java │ │ │ │ │ ├── AuthRequest.java │ │ │ │ │ ├── StatusUpdateRequest.java │ │ │ │ │ └── TaskRequest.java │ │ │ ├── exception │ │ │ │ └── TaskNotFoundException.java │ │ │ ├── mapper │ │ │ │ ├── TaskResponseMapper.java │ │ │ │ └── ManualTaskMapper.java │ │ │ └── entity │ │ │ │ └── Task.java │ │ │ ├── service │ │ │ ├── TaskStatusNotAvailableException.java │ │ │ ├── ReactiveTaskService.java │ │ │ └── TaskService.java │ │ │ ├── TaskManagementAppApplication.java │ │ │ ├── config │ │ │ └── OpenApiConfig.java │ │ │ ├── security │ │ │ ├── CustomUserDetailsService.java │ │ │ ├── JwtUtil.java │ │ │ ├── JwtAuthenticationFilter.java │ │ │ └── SecurityConfig.java │ │ │ ├── controller │ │ │ ├── AuthController.java │ │ │ ├── ReactiveTaskController.java │ │ │ └── TaskController.java │ │ │ └── repository │ │ │ └── TaskRepository.java │ └── resources │ │ └── application.yml └── test │ └── java │ └── tr │ └── com │ └── rsakin │ └── taskmanagementapp │ ├── TaskManagementAppApplicationTests.java │ ├── service │ └── TaskServiceTest.java │ └── controller │ └── TaskControllerTest.java ├── Dockerfile ├── .gitignore ├── docker-compose.yml ├── .mvn └── wrapper │ └── maven-wrapper.properties ├── README.md ├── pom.xml ├── mvnw.cmd └── mvnw /.gitattributes: -------------------------------------------------------------------------------- 1 | /mvnw text eol=lf 2 | *.cmd text eol=crlf 3 | -------------------------------------------------------------------------------- /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ramazansakin/task-management-app/HEAD/.DS_Store -------------------------------------------------------------------------------- /src/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ramazansakin/task-management-app/HEAD/src/.DS_Store -------------------------------------------------------------------------------- /src/main/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ramazansakin/task-management-app/HEAD/src/main/.DS_Store -------------------------------------------------------------------------------- /src/main/java/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ramazansakin/task-management-app/HEAD/src/main/java/.DS_Store -------------------------------------------------------------------------------- /src/main/java/tr/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ramazansakin/task-management-app/HEAD/src/main/java/tr/.DS_Store -------------------------------------------------------------------------------- /src/main/java/tr/com/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ramazansakin/task-management-app/HEAD/src/main/java/tr/com/.DS_Store -------------------------------------------------------------------------------- /src/main/java/tr/com/rsakin/taskmanagementapp/model/dto/response/AuthResponse.java: -------------------------------------------------------------------------------- 1 | package tr.com.rsakin.taskmanagementapp.model.dto.response; 2 | 3 | public record AuthResponse(String token) { 4 | } 5 | -------------------------------------------------------------------------------- /src/main/java/tr/com/rsakin/taskmanagementapp/model/dto/request/PriorityUpdateRequest.java: -------------------------------------------------------------------------------- 1 | package tr.com.rsakin.taskmanagementapp.model.dto.request; 2 | 3 | public record PriorityUpdateRequest(int value) { 4 | } 5 | -------------------------------------------------------------------------------- /src/main/java/tr/com/rsakin/taskmanagementapp/model/dto/request/AuthRequest.java: -------------------------------------------------------------------------------- 1 | package tr.com.rsakin.taskmanagementapp.model.dto.request; 2 | 3 | public record AuthRequest( 4 | String username, 5 | String password) { 6 | } 7 | -------------------------------------------------------------------------------- /src/main/java/tr/com/rsakin/taskmanagementapp/model/dto/request/StatusUpdateRequest.java: -------------------------------------------------------------------------------- 1 | package tr.com.rsakin.taskmanagementapp.model.dto.request; 2 | 3 | import tr.com.rsakin.taskmanagementapp.model.entity.Task; 4 | 5 | public record StatusUpdateRequest(Task.TaskStatus status) { 6 | } 7 | -------------------------------------------------------------------------------- /src/main/java/tr/com/rsakin/taskmanagementapp/service/TaskStatusNotAvailableException.java: -------------------------------------------------------------------------------- 1 | package tr.com.rsakin.taskmanagementapp.service; 2 | 3 | public class TaskStatusNotAvailableException extends RuntimeException { 4 | public TaskStatusNotAvailableException(String s) { 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/main/java/tr/com/rsakin/taskmanagementapp/model/dto/response/TaskStatistics.java: -------------------------------------------------------------------------------- 1 | package tr.com.rsakin.taskmanagementapp.model.dto.response; 2 | 3 | public record TaskStatistics( 4 | long total, 5 | long pending, 6 | long inProgress, 7 | long blocked, 8 | long completed) { 9 | } 10 | -------------------------------------------------------------------------------- /src/main/java/tr/com/rsakin/taskmanagementapp/model/exception/TaskNotFoundException.java: -------------------------------------------------------------------------------- 1 | package tr.com.rsakin.taskmanagementapp.model.exception; 2 | 3 | import java.util.UUID; 4 | 5 | public class TaskNotFoundException extends RuntimeException { 6 | public TaskNotFoundException(UUID id) { 7 | super("Task not found with ID: " + id); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/test/java/tr/com/rsakin/taskmanagementapp/TaskManagementAppApplicationTests.java: -------------------------------------------------------------------------------- 1 | package tr.com.rsakin.taskmanagementapp; 2 | 3 | import org.junit.jupiter.api.Test; 4 | import org.springframework.boot.test.context.SpringBootTest; 5 | 6 | @SpringBootTest 7 | class TaskManagementAppApplicationTests { 8 | 9 | @Test 10 | void contextLoads() { 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Dockerfile for Spring Boot App with JDK 17 2 | FROM openjdk:17-jdk-slim 3 | 4 | # Copy the packaged Spring Boot application JAR file into the container 5 | COPY target/*.jar app.jar 6 | 7 | # Expose the port your Spring Boot application runs on (typically 8080) 8 | EXPOSE 8080 9 | 10 | # Command to run the Spring Boot application 11 | ENTRYPOINT ["java", "-jar", "/app.jar"] -------------------------------------------------------------------------------- /src/main/java/tr/com/rsakin/taskmanagementapp/TaskManagementAppApplication.java: -------------------------------------------------------------------------------- 1 | package tr.com.rsakin.taskmanagementapp; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | 6 | @SpringBootApplication 7 | public class TaskManagementAppApplication { 8 | 9 | public static void main(String[] args) { 10 | SpringApplication.run(TaskManagementAppApplication.class, args); 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/tr/com/rsakin/taskmanagementapp/model/dto/response/TaskResponseDTO.java: -------------------------------------------------------------------------------- 1 | package tr.com.rsakin.taskmanagementapp.model.dto.response; 2 | 3 | import tr.com.rsakin.taskmanagementapp.model.entity.Task; 4 | 5 | import java.time.LocalDateTime; 6 | import java.util.UUID; 7 | 8 | public record TaskResponseDTO( 9 | UUID id, 10 | String title, 11 | String description, 12 | Task.TaskStatus status, 13 | LocalDateTime createdAt, 14 | Task.Priority priority 15 | ) {} 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | HELP.md 2 | target/ 3 | !.mvn/wrapper/maven-wrapper.jar 4 | !**/src/main/**/target/ 5 | !**/src/test/**/target/ 6 | 7 | ### STS ### 8 | .apt_generated 9 | .classpath 10 | .factorypath 11 | .project 12 | .settings 13 | .springBeans 14 | .sts4-cache 15 | 16 | ### IntelliJ IDEA ### 17 | .idea 18 | *.iws 19 | *.iml 20 | *.ipr 21 | 22 | ### NetBeans ### 23 | /nbproject/private/ 24 | /nbbuild/ 25 | /dist/ 26 | /nbdist/ 27 | /.nb-gradle/ 28 | build/ 29 | !**/src/main/**/build/ 30 | !**/src/test/**/build/ 31 | 32 | ### VS Code ### 33 | .vscode/ 34 | .DS_Store 35 | -------------------------------------------------------------------------------- /src/main/java/tr/com/rsakin/taskmanagementapp/model/dto/request/TaskRequest.java: -------------------------------------------------------------------------------- 1 | package tr.com.rsakin.taskmanagementapp.model.dto.request; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Data; 5 | import lombok.NoArgsConstructor; 6 | 7 | // DTO : Data Transfer Object 8 | // POJO : Plain Old Java Object 9 | // DTO vs POJO : 10 | // DTO is a data transfer object that is used to transfer data between the client and the server. 11 | // POJO is a plain old java object that is used to store data in memory. 12 | @Data 13 | @AllArgsConstructor 14 | @NoArgsConstructor 15 | public class TaskRequest { 16 | private String title; 17 | private String description; 18 | } 19 | -------------------------------------------------------------------------------- /src/main/java/tr/com/rsakin/taskmanagementapp/model/mapper/TaskResponseMapper.java: -------------------------------------------------------------------------------- 1 | package tr.com.rsakin.taskmanagementapp.model.mapper; 2 | 3 | import org.mapstruct.Mapper; 4 | import org.mapstruct.Mapping; 5 | import org.mapstruct.factory.Mappers; 6 | import tr.com.rsakin.taskmanagementapp.model.dto.response.TaskResponseDTO; 7 | import tr.com.rsakin.taskmanagementapp.model.entity.Task; 8 | 9 | import java.util.List; 10 | 11 | @Mapper(componentModel = "spring") 12 | public interface TaskResponseMapper { 13 | 14 | TaskResponseMapper INSTANCE = Mappers.getMapper(TaskResponseMapper.class); 15 | 16 | // @Mapping(target = "myTitle", source = "title") 17 | TaskResponseDTO toDTO(Task task); 18 | 19 | List toDTOList(List tasks); 20 | } 21 | 22 | -------------------------------------------------------------------------------- /src/main/java/tr/com/rsakin/taskmanagementapp/model/mapper/ManualTaskMapper.java: -------------------------------------------------------------------------------- 1 | package tr.com.rsakin.taskmanagementapp.model.mapper; 2 | 3 | import tr.com.rsakin.taskmanagementapp.model.dto.response.TaskResponseDTO; 4 | import tr.com.rsakin.taskmanagementapp.model.entity.Task; 5 | 6 | public class ManualTaskMapper { 7 | 8 | private ManualTaskMapper() {} 9 | 10 | public static TaskResponseDTO toDTO(Task task) { 11 | if (task == null) { 12 | return null; 13 | } 14 | return new TaskResponseDTO( 15 | task.getId(), 16 | task.getTitle(), 17 | task.getDescription(), 18 | task.getStatus(), 19 | task.getCreatedAt(), 20 | task.getPriority() 21 | ); 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | server: 2 | port: 8080 3 | 4 | spring: 5 | datasource: 6 | url: jdbc:postgresql://localhost:5432/taskdb 7 | username: postgres 8 | password: password 9 | driver-class-name: org.postgresql.Driver 10 | hikari: 11 | maximum-pool-size: 10 12 | minimum-idle: 5 13 | idle-timeout: 30000 14 | connection-timeout: 30000 15 | jpa: 16 | hibernate: 17 | ddl-auto: update 18 | show-sql: true 19 | properties: 20 | hibernate: 21 | format_sql: true 22 | dialect: org.hibernate.dialect.PostgreSQLDialect 23 | open-in-view: true 24 | application: 25 | name: task-management-app 26 | 27 | logging: 28 | level: 29 | org: 30 | hibernate: 31 | SQL: DEBUG 32 | type: 33 | descriptor: 34 | sql: 35 | BasicBinder: TRACE 36 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.8' 2 | 3 | services: 4 | postgres: 5 | image: postgres:16.3 6 | container_name: postgres_taskdb 7 | restart: always 8 | environment: 9 | POSTGRES_DB: taskdb 10 | POSTGRES_USER: postgres 11 | POSTGRES_PASSWORD: password 12 | ports: 13 | - "5432:5432" 14 | volumes: 15 | - postgres_data:/var/lib/postgresql/data 16 | healthcheck: 17 | test: ["CMD-SHELL", "pg_isready -U postgres -d taskdb"] 18 | interval: 10s 19 | timeout: 5s 20 | retries: 5 21 | 22 | app: 23 | build: 24 | context: . 25 | container_name: task-management-app 26 | ports: 27 | - "8080:8080" 28 | depends_on: 29 | postgres: 30 | condition: service_healthy 31 | environment: 32 | SPRING_DATASOURCE_URL: jdbc:postgresql://postgres:5432/taskdb 33 | SPRING_DATASOURCE_USERNAME: postgres 34 | SPRING_DATASOURCE_PASSWORD: password 35 | 36 | volumes: 37 | postgres_data: 38 | driver: local 39 | -------------------------------------------------------------------------------- /src/main/java/tr/com/rsakin/taskmanagementapp/config/OpenApiConfig.java: -------------------------------------------------------------------------------- 1 | package tr.com.rsakin.taskmanagementapp.config; 2 | 3 | import io.swagger.v3.oas.models.OpenAPI; 4 | import io.swagger.v3.oas.models.info.Contact; 5 | import io.swagger.v3.oas.models.info.Info; 6 | import org.springframework.context.annotation.Bean; 7 | import org.springframework.context.annotation.Configuration; 8 | 9 | 10 | @Configuration 11 | public class OpenApiConfig { 12 | 13 | @Bean 14 | public OpenAPI customOpenAPI() { 15 | return new OpenAPI() 16 | .info(new Info() 17 | .title("Task Management API") 18 | .description("API for managing tasks") 19 | .version("1.0.0") 20 | .contact(new Contact() 21 | .name("Ramazan Sakin") 22 | .email("ramazansakin63@gmail.com") 23 | .url("https://github.com/ramazansakin"))); 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /.mvn/wrapper/maven-wrapper.properties: -------------------------------------------------------------------------------- 1 | # Licensed to the Apache Software Foundation (ASF) under one 2 | # or more contributor license agreements. See the NOTICE file 3 | # distributed with this work for additional information 4 | # regarding copyright ownership. The ASF licenses this file 5 | # to you under the Apache License, Version 2.0 (the 6 | # "License"); you may not use this file except in compliance 7 | # with the License. You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, 12 | # software distributed under the License is distributed on an 13 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 14 | # KIND, either express or implied. See the License for the 15 | # specific language governing permissions and limitations 16 | # under the License. 17 | wrapperVersion=3.3.2 18 | distributionType=only-script 19 | distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.9/apache-maven-3.9.9-bin.zip 20 | -------------------------------------------------------------------------------- /src/main/java/tr/com/rsakin/taskmanagementapp/security/CustomUserDetailsService.java: -------------------------------------------------------------------------------- 1 | package tr.com.rsakin.taskmanagementapp.security; 2 | 3 | import org.springframework.security.core.userdetails.User; 4 | import org.springframework.security.core.userdetails.UserDetails; 5 | import org.springframework.security.core.userdetails.UserDetailsService; 6 | import org.springframework.security.core.userdetails.UsernameNotFoundException; 7 | import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; 8 | import org.springframework.stereotype.Component; 9 | 10 | @Component 11 | public class CustomUserDetailsService implements UserDetailsService { 12 | 13 | @Override 14 | public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { 15 | if (!username.equals("user")) { 16 | throw new UsernameNotFoundException("User not found"); 17 | } 18 | return User.builder() 19 | .username("user") 20 | .password(new BCryptPasswordEncoder().encode("password")) 21 | .roles("USER") 22 | .build(); 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Task Management API 2 | 3 | A simple task management API built with Spring Boot and Java 21. 4 | 5 | ## Overview 6 | 7 | This application provides REST endpoints to create, retrieve, update, and delete tasks. 8 | 9 | ## Tech Stack 10 | 11 | * Java 21 12 | * Spring Boot 13 | * OpenAPI/Swagger for documentation 14 | 15 | ## Getting Started 16 | 17 | ### Prerequisites 18 | 19 | * JDK 21 20 | * Maven or Gradle 21 | 22 | ### Running the Application 23 | 24 | ```bash 25 | # Using Maven 26 | mvn spring-boot:run 27 | 28 | # Using Gradle 29 | gradle bootRun 30 | ``` 31 | 32 | The application runs at `http://localhost:8080`. 33 | 34 | ## API Documentation 35 | 36 | Full API documentation is available via Swagger UI: 37 | 38 | Access the Swagger UI: `http://localhost:8080/swagger-ui.html` 39 | 40 | ## Main Endpoints 41 | 42 | * `POST /api/tasks` - Create a new task 43 | * `GET /api/tasks` - Get all tasks 44 | * `GET /api/tasks/{id}` - Get task by ID 45 | * `PATCH /api/tasks/{id}/status` - Update task status 46 | * `DELETE /api/tasks/{id}` - Delete a task 47 | * `GET /api/tasks/status/{status}` - Get tasks by status 48 | * `GET /api/tasks/title/{title}` - Find task by title 49 | 50 | ## Configuration 51 | 52 | Basic configuration can be adjusted in `application.properties` or `application.yml`. -------------------------------------------------------------------------------- /src/main/java/tr/com/rsakin/taskmanagementapp/security/JwtUtil.java: -------------------------------------------------------------------------------- 1 | package tr.com.rsakin.taskmanagementapp.security; 2 | 3 | import io.jsonwebtoken.Claims; 4 | import io.jsonwebtoken.Jwts; 5 | import io.jsonwebtoken.SignatureAlgorithm; 6 | import io.jsonwebtoken.security.Keys; 7 | import org.springframework.security.core.userdetails.UserDetails; 8 | import org.springframework.stereotype.Component; 9 | 10 | import java.security.Key; 11 | import java.util.Date; 12 | import java.util.function.Function; 13 | 14 | @Component 15 | public class JwtUtil { 16 | 17 | private final String SECRET_KEY = "mysecretkeymysecretkeymysecretkeymysecretkey"; 18 | private final long EXPIRATION_TIME = 1000 * 60 * 60; 19 | private final Key key = Keys.hmacShaKeyFor(SECRET_KEY.getBytes()); 20 | 21 | public String generateToken(UserDetails userDetails) { 22 | return Jwts.builder() 23 | .setSubject(userDetails.getUsername()) 24 | .setIssuedAt(new Date()) 25 | .setExpiration(new Date(System.currentTimeMillis() + EXPIRATION_TIME)) 26 | .signWith(key, SignatureAlgorithm.HS256) 27 | .compact(); 28 | } 29 | 30 | public String extractUsername(String token) { 31 | return extractClaim(token, Claims::getSubject); 32 | } 33 | 34 | public Date extractExpiration(String token) { 35 | return extractClaim(token, Claims::getExpiration); 36 | } 37 | 38 | public T extractClaim(String token, Function claimsResolver) { 39 | Claims claims = Jwts.parser().setSigningKey(key).build().parseClaimsJws(token).getBody(); 40 | return claimsResolver.apply(claims); 41 | } 42 | 43 | public boolean validateToken(String token, UserDetails userDetails) { 44 | return extractUsername(token).equals(userDetails.getUsername()) && !isTokenExpired(token); 45 | } 46 | 47 | private boolean isTokenExpired(String token) { 48 | return extractExpiration(token).before(new Date()); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/main/java/tr/com/rsakin/taskmanagementapp/controller/AuthController.java: -------------------------------------------------------------------------------- 1 | package tr.com.rsakin.taskmanagementapp.controller; 2 | 3 | import org.springframework.http.ResponseEntity; 4 | import org.springframework.security.authentication.AuthenticationManager; 5 | import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; 6 | import org.springframework.security.core.userdetails.UserDetails; 7 | import org.springframework.security.core.userdetails.UserDetailsService; 8 | import org.springframework.web.bind.annotation.PostMapping; 9 | import org.springframework.web.bind.annotation.RequestBody; 10 | import org.springframework.web.bind.annotation.RequestMapping; 11 | import org.springframework.web.bind.annotation.RestController; 12 | import tr.com.rsakin.taskmanagementapp.model.dto.request.AuthRequest; 13 | import tr.com.rsakin.taskmanagementapp.model.dto.response.AuthResponse; 14 | import tr.com.rsakin.taskmanagementapp.security.JwtUtil; 15 | 16 | @RestController 17 | @RequestMapping("/auth") 18 | public class AuthController { 19 | 20 | private final AuthenticationManager authenticationManager; 21 | private final JwtUtil jwtUtil; 22 | private final UserDetailsService userDetailsService; 23 | 24 | public AuthController(AuthenticationManager authenticationManager, JwtUtil jwtUtil, UserDetailsService userDetailsService) { 25 | this.authenticationManager = authenticationManager; 26 | this.jwtUtil = jwtUtil; 27 | this.userDetailsService = userDetailsService; 28 | } 29 | 30 | @PostMapping("/login") 31 | public ResponseEntity login(@RequestBody AuthRequest request) { 32 | authenticationManager.authenticate( 33 | new UsernamePasswordAuthenticationToken(request.username(), request.password()) 34 | ); 35 | UserDetails userDetails = userDetailsService.loadUserByUsername(request.username()); 36 | String token = jwtUtil.generateToken(userDetails); 37 | return ResponseEntity.ok(new AuthResponse(token)); 38 | } 39 | 40 | } 41 | -------------------------------------------------------------------------------- /src/main/java/tr/com/rsakin/taskmanagementapp/repository/TaskRepository.java: -------------------------------------------------------------------------------- 1 | package tr.com.rsakin.taskmanagementapp.repository; 2 | 3 | import org.springframework.data.jpa.repository.JpaRepository; 4 | import org.springframework.data.jpa.repository.Query; 5 | import org.springframework.data.repository.query.Param; 6 | import org.springframework.stereotype.Repository; 7 | import tr.com.rsakin.taskmanagementapp.model.entity.Task; 8 | 9 | import java.time.LocalDateTime; 10 | import java.util.List; 11 | import java.util.Optional; 12 | import java.util.UUID; 13 | 14 | @Repository 15 | public interface TaskRepository extends JpaRepository { 16 | 17 | // Standard JPA method queries 18 | List findByStatus(Task.TaskStatus status); 19 | 20 | List findByTitleContainingIgnoreCase(String title); 21 | 22 | Optional findFirstByOrderByCreatedAtDesc(); 23 | 24 | long countByStatus(Task.TaskStatus status); 25 | 26 | List findByCreatedAtBetween(LocalDateTime start, LocalDateTime end); 27 | 28 | // JPQL queries 29 | @Query("SELECT t FROM Task t WHERE t.priorityValue = :value ORDER BY t.createdAt DESC") 30 | List findTasksByPriorityValue(@Param("value") int priorityValue); 31 | 32 | @Query("SELECT t FROM Task t WHERE t.createdAt < :date AND t.status != 'COMPLETED'") 33 | List findOverdueTasks(@Param("date") LocalDateTime date); 34 | 35 | // Native SQL queries 36 | @Query(value = """ 37 | SELECT * FROM tasks 38 | WHERE status != 'COMPLETED' 39 | AND priority_value >= :minPriority 40 | ORDER BY priority_value DESC, created_at ASC 41 | LIMIT :limit 42 | """, nativeQuery = true) 43 | List findPriorityTasksToComplete( 44 | @Param("minPriority") int minPriority, 45 | @Param("limit") int limit); 46 | 47 | @Query(value = """ 48 | SELECT 49 | status, 50 | COUNT(*) as task_count, 51 | MIN(created_at) as oldest_task, 52 | MAX(created_at) as newest_task 53 | FROM tasks 54 | GROUP BY status 55 | """, nativeQuery = true) 56 | List getTaskStatusStatistics(); 57 | 58 | } 59 | -------------------------------------------------------------------------------- /src/main/java/tr/com/rsakin/taskmanagementapp/controller/ReactiveTaskController.java: -------------------------------------------------------------------------------- 1 | package tr.com.rsakin.taskmanagementapp.controller; 2 | 3 | import org.springframework.beans.factory.annotation.Autowired; 4 | import org.springframework.http.HttpStatus; 5 | import org.springframework.http.MediaType; 6 | import org.springframework.web.bind.annotation.*; 7 | import reactor.core.publisher.Flux; 8 | import reactor.core.publisher.Mono; 9 | import tr.com.rsakin.taskmanagementapp.model.dto.request.StatusUpdateRequest; 10 | import tr.com.rsakin.taskmanagementapp.model.dto.request.TaskRequest; 11 | import tr.com.rsakin.taskmanagementapp.model.entity.Task; 12 | import tr.com.rsakin.taskmanagementapp.model.exception.TaskNotFoundException; 13 | import tr.com.rsakin.taskmanagementapp.service.ReactiveTaskService; 14 | 15 | import java.util.UUID; 16 | 17 | @RestController 18 | @RequestMapping("/api/reactive/tasks") 19 | public class ReactiveTaskController { 20 | 21 | private final ReactiveTaskService taskService; 22 | 23 | 24 | @Autowired 25 | public ReactiveTaskController(ReactiveTaskService taskService) { 26 | this.taskService = taskService; 27 | } 28 | 29 | 30 | @PostMapping 31 | @ResponseStatus(HttpStatus.CREATED) 32 | public Mono createTask(@RequestBody TaskRequest request) { 33 | return taskService.createTask(request.getTitle(), request.getDescription()); 34 | } 35 | 36 | @GetMapping(produces = MediaType.TEXT_EVENT_STREAM_VALUE) 37 | public Flux getAllTasks() { 38 | return taskService.getAllTasks(); 39 | } 40 | 41 | @GetMapping("/{id}") 42 | public Mono getTaskById(@PathVariable UUID id) { 43 | return taskService.getTaskById(id) 44 | .switchIfEmpty(Mono.error(new TaskNotFoundException(id))); 45 | } 46 | 47 | @PatchMapping("/{id}/status") 48 | public Mono updateTaskStatus( 49 | @PathVariable UUID id, 50 | @RequestBody StatusUpdateRequest request) { 51 | 52 | return taskService.updateTaskStatus(id, request.status()) 53 | .onErrorResume(IllegalArgumentException.class, 54 | e -> Mono.error(new TaskNotFoundException(id))); 55 | } 56 | 57 | @DeleteMapping("/{id}") 58 | @ResponseStatus(HttpStatus.NO_CONTENT) 59 | public Mono deleteTask(@PathVariable UUID id) { 60 | return taskService.deleteTask(id); 61 | } 62 | 63 | } 64 | -------------------------------------------------------------------------------- /src/main/java/tr/com/rsakin/taskmanagementapp/security/JwtAuthenticationFilter.java: -------------------------------------------------------------------------------- 1 | package tr.com.rsakin.taskmanagementapp.security; 2 | 3 | import jakarta.servlet.FilterChain; 4 | import jakarta.servlet.ServletException; 5 | import jakarta.servlet.http.HttpServletRequest; 6 | import jakarta.servlet.http.HttpServletResponse; 7 | import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; 8 | import org.springframework.security.core.context.SecurityContextHolder; 9 | import org.springframework.security.core.userdetails.UserDetails; 10 | import org.springframework.security.core.userdetails.UserDetailsService; 11 | import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; 12 | import org.springframework.stereotype.Component; 13 | import org.springframework.web.filter.OncePerRequestFilter; 14 | 15 | import java.io.IOException; 16 | 17 | @Component 18 | class JwtAuthenticationFilter extends OncePerRequestFilter { 19 | private final JwtUtil jwtUtil; 20 | private final UserDetailsService userDetailsService; 21 | 22 | public JwtAuthenticationFilter(JwtUtil jwtUtil, UserDetailsService userDetailsService) { 23 | this.jwtUtil = jwtUtil; 24 | this.userDetailsService = userDetailsService; 25 | } 26 | 27 | @Override 28 | protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException { 29 | String authorizationHeader = request.getHeader("Authorization"); 30 | if (authorizationHeader != null && authorizationHeader.startsWith("Bearer ")) { 31 | String token = authorizationHeader.substring(7); 32 | String username = jwtUtil.extractUsername(token); 33 | 34 | if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) { 35 | UserDetails userDetails = userDetailsService.loadUserByUsername(username); 36 | 37 | if (jwtUtil.validateToken(token, userDetails)) { 38 | UsernamePasswordAuthenticationToken authentication = 39 | new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities()); 40 | 41 | authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); 42 | 43 | SecurityContextHolder.getContext().setAuthentication(authentication); 44 | } 45 | } 46 | } 47 | chain.doFilter(request, response); 48 | } 49 | } 50 | 51 | -------------------------------------------------------------------------------- /src/main/java/tr/com/rsakin/taskmanagementapp/security/SecurityConfig.java: -------------------------------------------------------------------------------- 1 | package tr.com.rsakin.taskmanagementapp.security; 2 | 3 | import org.springframework.context.annotation.Bean; 4 | import org.springframework.context.annotation.Configuration; 5 | import org.springframework.context.annotation.Lazy; 6 | import org.springframework.security.authentication.AuthenticationManager; 7 | import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration; 8 | import org.springframework.security.config.annotation.web.builders.HttpSecurity; 9 | import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; 10 | import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; 11 | import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; 12 | import org.springframework.security.crypto.password.PasswordEncoder; 13 | import org.springframework.security.web.SecurityFilterChain; 14 | import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; 15 | 16 | @Configuration 17 | @EnableWebSecurity 18 | class SecurityConfig { 19 | private final JwtAuthenticationFilter jwtAuthenticationFilter; 20 | 21 | public SecurityConfig(@Lazy JwtAuthenticationFilter jwtAuthenticationFilter) { 22 | this.jwtAuthenticationFilter = jwtAuthenticationFilter; 23 | } 24 | 25 | // Swagger URL - http://localhost:8080/swagger-ui/index.html 26 | @Bean 27 | public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { 28 | http 29 | .csrf(AbstractHttpConfigurer::disable) 30 | .cors(AbstractHttpConfigurer::disable) 31 | .authorizeHttpRequests(auth -> auth 32 | .requestMatchers("/auth/login").permitAll() // ✅ Allow public access to login 33 | .requestMatchers("/swagger-ui/**", "/v3/api-docs/**").permitAll() // ✅ Allow public access to swagger 34 | .anyRequest().authenticated() // 🔒 Secure all other endpoints 35 | ) 36 | .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class); 37 | 38 | return http.build(); 39 | } 40 | 41 | @Bean 42 | public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception { 43 | return authenticationConfiguration.getAuthenticationManager(); 44 | } 45 | 46 | @Bean 47 | public PasswordEncoder passwordEncoder() { 48 | return new BCryptPasswordEncoder(); 49 | } 50 | } 51 | 52 | -------------------------------------------------------------------------------- /src/main/java/tr/com/rsakin/taskmanagementapp/service/ReactiveTaskService.java: -------------------------------------------------------------------------------- 1 | package tr.com.rsakin.taskmanagementapp.service; 2 | 3 | import org.springframework.stereotype.Service; 4 | import reactor.core.publisher.Flux; 5 | import reactor.core.publisher.Mono; 6 | import tr.com.rsakin.taskmanagementapp.model.entity.Task; 7 | 8 | import java.time.Duration; 9 | import java.util.HashMap; 10 | import java.util.Map; 11 | import java.util.UUID; 12 | 13 | // Reactive Programming Concepts: 14 | // - Asynchronous: Enables handling multiple requests simultaneously without blocking the main thread. 15 | // - Non-blocking: Doesn't wait for a response before continuing execution, improving efficiency. 16 | // - Reactive Streams: A specification that allows handling data streams efficiently with built-in mechanisms for processing data reactively. 17 | // - Backpressure: A flow control mechanism that prevents overwhelming consumers by regulating the rate of data emission. 18 | @Service 19 | public class ReactiveTaskService { 20 | // In-memory storage (will replace with reactive DB in later weeks) 21 | // HashMap vs LinkedHashMap: 22 | // HashMap is a non-thread-safe implementation, while LinkedHashMap is thread-safe and provides better performance for concurrent operations. 23 | // HashMap is faster for non-concurrent operations, while LinkedHashMap is faster for concurrent operations. 24 | // LinkedHashMap is more memory-efficient than HashMap, as it keeps the order of insertion. 25 | // We can use whether HashMap or LinkedHashMap depending on the requirements 26 | private final Map taskStore = new HashMap<>(); 27 | 28 | public Mono createTask(String title, String description) { 29 | return Mono.fromCallable(() -> { 30 | // Clean code: Validation 31 | if (title == null || title.trim().isEmpty()) { 32 | throw new IllegalArgumentException("Task title cannot be empty"); 33 | } 34 | 35 | if (description == null) { 36 | throw new IllegalArgumentException("Task description cannot be null"); 37 | } 38 | 39 | Task task = Task.builder() 40 | .title(title) 41 | .description(description) 42 | .build(); 43 | 44 | taskStore.put(task.getId(), task); 45 | return task; 46 | }); 47 | } 48 | 49 | public Flux getAllTasks() { 50 | // return Flux.fromIterable(taskStore.values()); 51 | return Flux.fromIterable(taskStore.values()) 52 | .delayElements(Duration.ofSeconds(1)); 53 | } 54 | 55 | public Mono getTaskById(UUID id) { 56 | return Mono.justOrEmpty(taskStore.get(id)); 57 | } 58 | 59 | public Mono updateTaskStatus(UUID id, Task.TaskStatus newStatus) { 60 | return Mono.justOrEmpty(taskStore.get(id)) 61 | .switchIfEmpty(Mono.error(new IllegalArgumentException("Task not found with ID: " + id))) 62 | .map(task -> { 63 | Task updatedTask = task.updateStatus(newStatus); 64 | taskStore.put(id, updatedTask); 65 | return updatedTask; 66 | }); 67 | } 68 | 69 | public Mono deleteTask(UUID id) { 70 | return Mono.fromRunnable(() -> taskStore.remove(id)); 71 | } 72 | 73 | } 74 | -------------------------------------------------------------------------------- /src/main/java/tr/com/rsakin/taskmanagementapp/model/entity/Task.java: -------------------------------------------------------------------------------- 1 | package tr.com.rsakin.taskmanagementapp.model.entity; 2 | 3 | import jakarta.persistence.*; 4 | import lombok.*; 5 | 6 | import java.time.LocalDateTime; 7 | import java.util.UUID; 8 | 9 | // Entity class 10 | // Clean code: Immutability principle 11 | // Clean code: Builder pattern 12 | 13 | @Entity 14 | @Table(name = "tasks") 15 | @Getter 16 | @NoArgsConstructor // Required by JPA 17 | @AllArgsConstructor // Used by the builder 18 | @Builder 19 | public class Task { 20 | @Id 21 | @Column(name = "id", updatable = false, nullable = false) 22 | private UUID id; 23 | 24 | @Column(name = "title", nullable = false) 25 | private String title; 26 | 27 | @Column(name = "description", length = 2000) 28 | private String description; 29 | 30 | @Column(name = "status", nullable = false) 31 | @Enumerated(EnumType.STRING) 32 | @With 33 | private TaskStatus status; 34 | 35 | @Column(name = "created_at", nullable = false, updatable = false) 36 | private LocalDateTime createdAt; 37 | 38 | @Column(name = "updated_at") 39 | private LocalDateTime updatedAt; 40 | 41 | @Column(name = "priority_value") 42 | private int priorityValue; 43 | 44 | @Column(name = "priority_label") 45 | private String priorityLabel; 46 | 47 | public Task.Priority getPriority() { 48 | return switch (status) { 49 | case PENDING, COMPLETED -> new LowPriority(); 50 | case IN_PROGRESS -> new MediumPriority(); 51 | case BLOCKED -> new HighPriority(); 52 | }; 53 | } 54 | 55 | // JPA Lifecycle methods 56 | @PrePersist 57 | protected void onCreate() { 58 | if (id == null) { 59 | id = UUID.randomUUID(); 60 | } 61 | if (createdAt == null) { 62 | createdAt = LocalDateTime.now(); 63 | } 64 | updatedAt = LocalDateTime.now(); 65 | 66 | // Set priority fields based on status 67 | updatePriorityFields(); 68 | } 69 | 70 | private void updatePriorityFields() { 71 | Priority priority = switch (status) { 72 | case PENDING, COMPLETED -> new LowPriority(); 73 | case IN_PROGRESS -> new MediumPriority(); 74 | case BLOCKED -> new HighPriority(); 75 | }; 76 | 77 | this.priorityValue = priority.getValue(); 78 | this.priorityLabel = priority.getLabel(); 79 | } 80 | 81 | // Lombok builder with default values 82 | public static class TaskBuilder { 83 | private UUID id = UUID.randomUUID(); 84 | private TaskStatus status = TaskStatus.PENDING; 85 | private LocalDateTime createdAt = LocalDateTime.now(); 86 | } 87 | 88 | // Immutable status update using Lombok's @With 89 | public Task updateStatus(TaskStatus newStatus) { 90 | Task updated = this.withStatus(newStatus); 91 | updated.updatedAt = LocalDateTime.now(); 92 | updated.updatePriorityFields(); 93 | return updated; 94 | } 95 | 96 | // Using Java 17 sealed classes for task status 97 | public enum TaskStatus { 98 | PENDING, IN_PROGRESS, BLOCKED, COMPLETED 99 | } 100 | 101 | // Java 17 sealed classes hierarchy for task priorities 102 | public sealed interface Priority permits LowPriority, MediumPriority, HighPriority { 103 | String getLabel(); 104 | int getValue(); 105 | } 106 | 107 | public enum TaskPriority { 108 | LOW( 1, "Low"), 109 | MEDIUM(2, "Medium"), 110 | HIGH(3, "High"); 111 | 112 | private final int value; 113 | private final String label; 114 | 115 | TaskPriority(int value, String label) { 116 | this.value = value; 117 | this.label = label; 118 | } 119 | } 120 | 121 | public static final class LowPriority implements Priority { 122 | @Override 123 | public String getLabel() { 124 | return TaskPriority.LOW.label; 125 | } 126 | 127 | @Override 128 | public int getValue() { 129 | return TaskPriority.LOW.value; 130 | } 131 | } 132 | 133 | public static final class MediumPriority implements Priority { 134 | @Override 135 | public String getLabel() { 136 | return TaskPriority.MEDIUM.label; 137 | } 138 | 139 | @Override 140 | public int getValue() { 141 | return TaskPriority.MEDIUM.value; 142 | } 143 | } 144 | 145 | public static final class HighPriority implements Priority { 146 | @Override 147 | public String getLabel() { 148 | return TaskPriority.HIGH.label; 149 | } 150 | 151 | @Override 152 | public int getValue() { 153 | return TaskPriority.HIGH.value; 154 | } 155 | } 156 | 157 | } 158 | -------------------------------------------------------------------------------- /src/test/java/tr/com/rsakin/taskmanagementapp/service/TaskServiceTest.java: -------------------------------------------------------------------------------- 1 | package tr.com.rsakin.taskmanagementapp.service; 2 | 3 | import org.junit.jupiter.api.BeforeEach; 4 | import org.junit.jupiter.api.Test; 5 | import org.junit.jupiter.api.extension.ExtendWith; 6 | import org.mockito.InjectMocks; 7 | import org.mockito.Mock; 8 | import org.mockito.junit.jupiter.MockitoExtension; 9 | import tr.com.rsakin.taskmanagementapp.model.dto.request.TaskRequest; 10 | import tr.com.rsakin.taskmanagementapp.model.dto.response.TaskResponseDTO; 11 | import tr.com.rsakin.taskmanagementapp.model.entity.Task; 12 | import tr.com.rsakin.taskmanagementapp.repository.TaskRepository; 13 | 14 | import java.time.LocalDateTime; 15 | import java.util.Collections; 16 | import java.util.List; 17 | import java.util.Optional; 18 | import java.util.UUID; 19 | 20 | import static org.junit.jupiter.api.Assertions.*; 21 | import static org.mockito.Mockito.*; 22 | 23 | @ExtendWith(MockitoExtension.class) 24 | class TaskServiceTest { 25 | 26 | @Mock 27 | private TaskRepository taskRepository; 28 | 29 | @InjectMocks 30 | private TaskService taskService; 31 | 32 | @Test 33 | void shouldCreateTask() { 34 | TaskRequest request = new TaskRequest("Test Task", "Description"); 35 | Task task = Task.builder().id(UUID.randomUUID()).title("Test Task").description("Description").build(); 36 | TaskResponseDTO responseDTO = new TaskResponseDTO(task.getId(), task.getTitle(), task.getDescription(), task.getStatus(), task.getCreatedAt(), task.getPriority()); 37 | 38 | when(taskRepository.save(any(Task.class))).thenReturn(task); 39 | 40 | TaskResponseDTO createdTask = taskService.createTask(request.getTitle(), request.getDescription()); 41 | 42 | assertNotNull(createdTask); 43 | assertEquals("Test Task", createdTask.title()); 44 | verify(taskRepository).save(any(Task.class)); 45 | } 46 | 47 | @Test 48 | void shouldReturnAllTasks() { 49 | Task task = new Task(UUID.randomUUID(), "Task 1", "Description", Task.TaskStatus.PENDING, LocalDateTime.now(), LocalDateTime.now(), 1, "Low"); 50 | List tasks = Collections.singletonList(task); 51 | when(taskRepository.findAll()).thenReturn(tasks); 52 | 53 | List taskResponseDTOList = taskService.getAllTasks(); 54 | 55 | assertNotNull(taskResponseDTOList); 56 | assertEquals(1, taskResponseDTOList.size()); 57 | } 58 | 59 | @Test 60 | void shouldReturnNullWhenTaskNotFoundById() { 61 | UUID nonExistingId = UUID.randomUUID(); 62 | when(taskRepository.findById(nonExistingId)).thenReturn(Optional.empty()); 63 | 64 | TaskResponseDTO task = taskService.getTaskById(nonExistingId); 65 | 66 | assertNull(task); 67 | } 68 | 69 | @Test 70 | void shouldUpdateTaskStatus() { 71 | UUID taskId = UUID.randomUUID(); 72 | Task task = new Task(taskId, "Task 1", "Description", Task.TaskStatus.PENDING, LocalDateTime.now(), LocalDateTime.now(), 1, "Low"); 73 | Task updatedTask = task.updateStatus(Task.TaskStatus.COMPLETED); 74 | 75 | when(taskRepository.findById(taskId)).thenReturn(Optional.of(task)); 76 | when(taskRepository.save(any(Task.class))).thenReturn(updatedTask); 77 | 78 | Task savedTask = taskService.updateTaskStatus(taskId, Task.TaskStatus.COMPLETED); 79 | 80 | assertNotNull(savedTask); 81 | assertEquals(Task.TaskStatus.COMPLETED, savedTask.getStatus()); 82 | verify(taskRepository).save(any(Task.class)); 83 | } 84 | 85 | @Test 86 | void shouldThrowExceptionWhenTaskNotFoundForUpdate() { 87 | UUID nonExistingId = UUID.randomUUID(); 88 | when(taskRepository.findById(nonExistingId)).thenReturn(Optional.empty()); 89 | 90 | assertThrows(IllegalArgumentException.class, () -> taskService.updateTaskStatus(nonExistingId, Task.TaskStatus.COMPLETED)); 91 | } 92 | 93 | @Test 94 | void shouldReturnTasksByStatus() { 95 | Task task = new Task(UUID.randomUUID(), "Task 1", "Description", Task.TaskStatus.PENDING, LocalDateTime.now(), LocalDateTime.now(), 1, "Low"); 96 | List tasks = Collections.singletonList(task); 97 | when(taskRepository.findByStatus(Task.TaskStatus.PENDING)).thenReturn(tasks); 98 | 99 | List taskResponseDTOList = taskService.getTasksByStatus(Task.TaskStatus.PENDING); 100 | 101 | assertNotNull(taskResponseDTOList); 102 | assertEquals(1, taskResponseDTOList.size()); 103 | } 104 | 105 | @Test 106 | void shouldReturnEmptyListWhenNoTasks() { 107 | when(taskRepository.findAll()).thenReturn(Collections.emptyList()); 108 | 109 | List taskResponseDTOList = taskService.getAllTasks(); 110 | 111 | assertTrue(taskResponseDTOList.isEmpty()); 112 | } 113 | 114 | } 115 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | org.springframework.boot 7 | spring-boot-starter-parent 8 | 3.4.3 9 | 10 | 11 | tr.com.rsakin 12 | task-management-app 13 | 0.0.1-SNAPSHOT 14 | task-management-app 15 | task-management-app 16 | 17 | 18 | 21 19 | 20 | 21 | 22 | 23 | org.springframework.boot 24 | spring-boot-starter-web 25 | 26 | 27 | org.springframework.boot 28 | spring-boot-starter-webflux 29 | 30 | 31 | 32 | org.springframework.boot 33 | spring-boot-devtools 34 | runtime 35 | true 36 | 37 | 38 | org.springframework.boot 39 | spring-boot-starter-test 40 | test 41 | 42 | 43 | io.projectreactor 44 | reactor-test 45 | test 46 | 47 | 48 | org.mapstruct 49 | mapstruct 50 | 1.6.3 51 | 52 | 53 | 54 | 55 | org.springframework.boot 56 | spring-boot-starter-data-jpa 57 | 58 | 59 | 60 | 61 | org.postgresql 62 | postgresql 63 | runtime 64 | 65 | 66 | 67 | 68 | org.springframework.boot 69 | spring-boot-starter-security 70 | 71 | 72 | 73 | 74 | io.jsonwebtoken 75 | jjwt 76 | 0.12.3 77 | 78 | 79 | 80 | 81 | org.springframework.boot 82 | spring-boot-starter-validation 83 | 84 | 85 | 86 | org.springdoc 87 | springdoc-openapi-starter-webmvc-ui 88 | 2.3.0 89 | 90 | 91 | 92 | 93 | org.mapstruct 94 | mapstruct-processor 95 | 1.6.3 96 | provided 97 | 98 | 99 | 100 | 101 | org.projectlombok 102 | lombok 103 | 1.18.36 104 | provided 105 | 106 | 107 | 108 | 109 | org.projectlombok 110 | lombok-mapstruct-binding 111 | 0.2.0 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | org.apache.maven.plugins 120 | maven-compiler-plugin 121 | 3.8.1 122 | 123 | 21 124 | 21 125 | 126 | 127 | org.mapstruct 128 | mapstruct-processor 129 | 1.6.3 130 | 131 | 132 | org.projectlombok 133 | lombok 134 | 1.18.36 135 | 136 | 137 | org.projectlombok 138 | lombok-mapstruct-binding 139 | 0.2.0 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | -------------------------------------------------------------------------------- /src/test/java/tr/com/rsakin/taskmanagementapp/controller/TaskControllerTest.java: -------------------------------------------------------------------------------- 1 | package tr.com.rsakin.taskmanagementapp.controller; 2 | 3 | import org.junit.jupiter.api.BeforeEach; 4 | import org.junit.jupiter.api.Test; 5 | import org.junit.jupiter.api.extension.ExtendWith; 6 | import org.mockito.InjectMocks; 7 | import org.mockito.Mock; 8 | import org.mockito.junit.jupiter.MockitoExtension; 9 | import org.springframework.http.HttpStatus; 10 | import org.springframework.http.ResponseEntity; 11 | import tr.com.rsakin.taskmanagementapp.model.dto.request.StatusUpdateRequest; 12 | import tr.com.rsakin.taskmanagementapp.model.dto.request.TaskRequest; 13 | import tr.com.rsakin.taskmanagementapp.model.dto.response.TaskResponseDTO; 14 | import tr.com.rsakin.taskmanagementapp.model.entity.Task; 15 | import tr.com.rsakin.taskmanagementapp.service.TaskService; 16 | 17 | import java.time.LocalDateTime; 18 | import java.util.Collections; 19 | import java.util.List; 20 | import java.util.UUID; 21 | 22 | import static org.junit.jupiter.api.Assertions.*; 23 | import static org.mockito.ArgumentMatchers.anyString; 24 | import static org.mockito.Mockito.*; 25 | 26 | @ExtendWith(MockitoExtension.class) 27 | class TaskControllerTest { 28 | 29 | @Mock 30 | private TaskService taskService; 31 | 32 | @InjectMocks 33 | private TaskController taskController; 34 | 35 | @Test 36 | void shouldCreateTask() { 37 | TaskRequest request = new TaskRequest("Test Task", "Description"); 38 | TaskResponseDTO taskResponseDTO = new TaskResponseDTO(UUID.randomUUID(), "Test Task", 39 | "Description", Task.TaskStatus.PENDING, LocalDateTime.now(), new Task.LowPriority()); 40 | 41 | when(taskService.createTask(anyString(), anyString())).thenReturn(taskResponseDTO); 42 | 43 | ResponseEntity response = taskController.createTask(request); 44 | 45 | assertEquals(HttpStatus.CREATED, response.getStatusCode()); 46 | assertNotNull(response.getBody()); 47 | assertEquals("Test Task", response.getBody().title()); 48 | } 49 | 50 | @Test 51 | void shouldGetAllTasks() { 52 | TaskResponseDTO taskResponseDTO = new TaskResponseDTO(UUID.randomUUID(), "Test Task", 53 | "Description", Task.TaskStatus.PENDING, LocalDateTime.now(), new Task.LowPriority()); 54 | List taskResponseDTOList = Collections.singletonList(taskResponseDTO); 55 | 56 | when(taskService.getAllTasks()).thenReturn(taskResponseDTOList); 57 | 58 | ResponseEntity> response = taskController.getAllTasks(); 59 | 60 | assertEquals(HttpStatus.OK, response.getStatusCode()); 61 | assertFalse(response.getBody().isEmpty()); 62 | } 63 | 64 | @Test 65 | void shouldGetTaskById() { 66 | UUID taskId = UUID.randomUUID(); 67 | TaskResponseDTO taskResponseDTO = new TaskResponseDTO(taskId, "Test Task", 68 | "Description", Task.TaskStatus.PENDING, LocalDateTime.now(), new Task.LowPriority()); 69 | 70 | when(taskService.getTaskById(taskId)).thenReturn(taskResponseDTO); 71 | 72 | ResponseEntity response = taskController.getTaskById(taskId); 73 | 74 | assertEquals(HttpStatus.OK, response.getStatusCode()); 75 | assertNotNull(response.getBody()); 76 | assertEquals(taskId, response.getBody().id()); 77 | } 78 | 79 | @Test 80 | void shouldReturnNotFoundWhenTaskByIdDoesNotExist() { 81 | UUID nonExistingId = UUID.randomUUID(); 82 | 83 | when(taskService.getTaskById(nonExistingId)).thenReturn(null); 84 | 85 | ResponseEntity response = taskController.getTaskById(nonExistingId); 86 | 87 | assertEquals(HttpStatus.NOT_FOUND, response.getStatusCode()); 88 | } 89 | 90 | @Test 91 | void shouldUpdateTaskStatus() { 92 | UUID taskId = UUID.randomUUID(); 93 | StatusUpdateRequest request = new StatusUpdateRequest(Task.TaskStatus.COMPLETED); 94 | Task updatedTask = new Task(taskId, "Test Task", "Description", Task.TaskStatus.COMPLETED, LocalDateTime.now(), LocalDateTime.now(), 1, "Low"); 95 | 96 | when(taskService.updateTaskStatus(taskId, Task.TaskStatus.COMPLETED)).thenReturn(updatedTask); 97 | 98 | ResponseEntity response = taskController.updateTaskStatus(taskId, request); 99 | 100 | assertEquals(HttpStatus.OK, response.getStatusCode()); 101 | assertEquals(Task.TaskStatus.COMPLETED, response.getBody().getStatus()); 102 | } 103 | 104 | @Test 105 | void shouldReturnNotFoundWhenTaskForStatusUpdateDoesNotExist() { 106 | UUID nonExistingId = UUID.randomUUID(); 107 | StatusUpdateRequest request = new StatusUpdateRequest(Task.TaskStatus.COMPLETED); 108 | 109 | when(taskService.updateTaskStatus(nonExistingId, Task.TaskStatus.COMPLETED)).thenThrow(IllegalArgumentException.class); 110 | 111 | ResponseEntity response = taskController.updateTaskStatus(nonExistingId, request); 112 | 113 | assertEquals(HttpStatus.NOT_FOUND, response.getStatusCode()); 114 | } 115 | 116 | @Test 117 | void shouldDeleteTask() { 118 | UUID taskId = UUID.randomUUID(); 119 | doNothing().when(taskService).deleteTask(taskId); 120 | 121 | ResponseEntity response = taskController.deleteTask(taskId); 122 | 123 | assertEquals(HttpStatus.NO_CONTENT, response.getStatusCode()); 124 | verify(taskService).deleteTask(taskId); 125 | } 126 | 127 | @Test 128 | void shouldGetTasksByStatus() { 129 | TaskResponseDTO taskResponseDTO = new TaskResponseDTO(UUID.randomUUID(), "Test Task", "Description", Task.TaskStatus.PENDING, LocalDateTime.now(), new Task.LowPriority()); 130 | List taskResponseDTOList = Collections.singletonList(taskResponseDTO); 131 | 132 | when(taskService.getTasksByStatus(Task.TaskStatus.PENDING)).thenReturn(taskResponseDTOList); 133 | 134 | ResponseEntity> response = taskController.getTasksByStatus(Task.TaskStatus.PENDING); 135 | 136 | assertEquals(HttpStatus.OK, response.getStatusCode()); 137 | assertFalse(response.getBody().isEmpty()); 138 | } 139 | 140 | } 141 | 142 | 143 | -------------------------------------------------------------------------------- /mvnw.cmd: -------------------------------------------------------------------------------- 1 | <# : batch portion 2 | @REM ---------------------------------------------------------------------------- 3 | @REM Licensed to the Apache Software Foundation (ASF) under one 4 | @REM or more contributor license agreements. See the NOTICE file 5 | @REM distributed with this work for additional information 6 | @REM regarding copyright ownership. The ASF licenses this file 7 | @REM to you under the Apache License, Version 2.0 (the 8 | @REM "License"); you may not use this file except in compliance 9 | @REM with the License. You may obtain a copy of the License at 10 | @REM 11 | @REM http://www.apache.org/licenses/LICENSE-2.0 12 | @REM 13 | @REM Unless required by applicable law or agreed to in writing, 14 | @REM software distributed under the License is distributed on an 15 | @REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 16 | @REM KIND, either express or implied. See the License for the 17 | @REM specific language governing permissions and limitations 18 | @REM under the License. 19 | @REM ---------------------------------------------------------------------------- 20 | 21 | @REM ---------------------------------------------------------------------------- 22 | @REM Apache Maven Wrapper startup batch script, version 3.3.2 23 | @REM 24 | @REM Optional ENV vars 25 | @REM MVNW_REPOURL - repo url base for downloading maven distribution 26 | @REM MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven 27 | @REM MVNW_VERBOSE - true: enable verbose log; others: silence the output 28 | @REM ---------------------------------------------------------------------------- 29 | 30 | @IF "%__MVNW_ARG0_NAME__%"=="" (SET __MVNW_ARG0_NAME__=%~nx0) 31 | @SET __MVNW_CMD__= 32 | @SET __MVNW_ERROR__= 33 | @SET __MVNW_PSMODULEP_SAVE=%PSModulePath% 34 | @SET PSModulePath= 35 | @FOR /F "usebackq tokens=1* delims==" %%A IN (`powershell -noprofile "& {$scriptDir='%~dp0'; $script='%__MVNW_ARG0_NAME__%'; icm -ScriptBlock ([Scriptblock]::Create((Get-Content -Raw '%~f0'))) -NoNewScope}"`) DO @( 36 | IF "%%A"=="MVN_CMD" (set __MVNW_CMD__=%%B) ELSE IF "%%B"=="" (echo %%A) ELSE (echo %%A=%%B) 37 | ) 38 | @SET PSModulePath=%__MVNW_PSMODULEP_SAVE% 39 | @SET __MVNW_PSMODULEP_SAVE= 40 | @SET __MVNW_ARG0_NAME__= 41 | @SET MVNW_USERNAME= 42 | @SET MVNW_PASSWORD= 43 | @IF NOT "%__MVNW_CMD__%"=="" (%__MVNW_CMD__% %*) 44 | @echo Cannot start maven from wrapper >&2 && exit /b 1 45 | @GOTO :EOF 46 | : end batch / begin powershell #> 47 | 48 | $ErrorActionPreference = "Stop" 49 | if ($env:MVNW_VERBOSE -eq "true") { 50 | $VerbosePreference = "Continue" 51 | } 52 | 53 | # calculate distributionUrl, requires .mvn/wrapper/maven-wrapper.properties 54 | $distributionUrl = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionUrl 55 | if (!$distributionUrl) { 56 | Write-Error "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties" 57 | } 58 | 59 | switch -wildcard -casesensitive ( $($distributionUrl -replace '^.*/','') ) { 60 | "maven-mvnd-*" { 61 | $USE_MVND = $true 62 | $distributionUrl = $distributionUrl -replace '-bin\.[^.]*$',"-windows-amd64.zip" 63 | $MVN_CMD = "mvnd.cmd" 64 | break 65 | } 66 | default { 67 | $USE_MVND = $false 68 | $MVN_CMD = $script -replace '^mvnw','mvn' 69 | break 70 | } 71 | } 72 | 73 | # apply MVNW_REPOURL and calculate MAVEN_HOME 74 | # maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ 75 | if ($env:MVNW_REPOURL) { 76 | $MVNW_REPO_PATTERN = if ($USE_MVND) { "/org/apache/maven/" } else { "/maven/mvnd/" } 77 | $distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace '^.*'+$MVNW_REPO_PATTERN,'')" 78 | } 79 | $distributionUrlName = $distributionUrl -replace '^.*/','' 80 | $distributionUrlNameMain = $distributionUrlName -replace '\.[^.]*$','' -replace '-bin$','' 81 | $MAVEN_HOME_PARENT = "$HOME/.m2/wrapper/dists/$distributionUrlNameMain" 82 | if ($env:MAVEN_USER_HOME) { 83 | $MAVEN_HOME_PARENT = "$env:MAVEN_USER_HOME/wrapper/dists/$distributionUrlNameMain" 84 | } 85 | $MAVEN_HOME_NAME = ([System.Security.Cryptography.MD5]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join '' 86 | $MAVEN_HOME = "$MAVEN_HOME_PARENT/$MAVEN_HOME_NAME" 87 | 88 | if (Test-Path -Path "$MAVEN_HOME" -PathType Container) { 89 | Write-Verbose "found existing MAVEN_HOME at $MAVEN_HOME" 90 | Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" 91 | exit $? 92 | } 93 | 94 | if (! $distributionUrlNameMain -or ($distributionUrlName -eq $distributionUrlNameMain)) { 95 | Write-Error "distributionUrl is not valid, must end with *-bin.zip, but found $distributionUrl" 96 | } 97 | 98 | # prepare tmp dir 99 | $TMP_DOWNLOAD_DIR_HOLDER = New-TemporaryFile 100 | $TMP_DOWNLOAD_DIR = New-Item -Itemtype Directory -Path "$TMP_DOWNLOAD_DIR_HOLDER.dir" 101 | $TMP_DOWNLOAD_DIR_HOLDER.Delete() | Out-Null 102 | trap { 103 | if ($TMP_DOWNLOAD_DIR.Exists) { 104 | try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } 105 | catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } 106 | } 107 | } 108 | 109 | New-Item -Itemtype Directory -Path "$MAVEN_HOME_PARENT" -Force | Out-Null 110 | 111 | # Download and Install Apache Maven 112 | Write-Verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." 113 | Write-Verbose "Downloading from: $distributionUrl" 114 | Write-Verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" 115 | 116 | $webclient = New-Object System.Net.WebClient 117 | if ($env:MVNW_USERNAME -and $env:MVNW_PASSWORD) { 118 | $webclient.Credentials = New-Object System.Net.NetworkCredential($env:MVNW_USERNAME, $env:MVNW_PASSWORD) 119 | } 120 | [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 121 | $webclient.DownloadFile($distributionUrl, "$TMP_DOWNLOAD_DIR/$distributionUrlName") | Out-Null 122 | 123 | # If specified, validate the SHA-256 sum of the Maven distribution zip file 124 | $distributionSha256Sum = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionSha256Sum 125 | if ($distributionSha256Sum) { 126 | if ($USE_MVND) { 127 | Write-Error "Checksum validation is not supported for maven-mvnd. `nPlease disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." 128 | } 129 | Import-Module $PSHOME\Modules\Microsoft.PowerShell.Utility -Function Get-FileHash 130 | if ((Get-FileHash "$TMP_DOWNLOAD_DIR/$distributionUrlName" -Algorithm SHA256).Hash.ToLower() -ne $distributionSha256Sum) { 131 | Write-Error "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised. If you updated your Maven version, you need to update the specified distributionSha256Sum property." 132 | } 133 | } 134 | 135 | # unzip and move 136 | Expand-Archive "$TMP_DOWNLOAD_DIR/$distributionUrlName" -DestinationPath "$TMP_DOWNLOAD_DIR" | Out-Null 137 | Rename-Item -Path "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" -NewName $MAVEN_HOME_NAME | Out-Null 138 | try { 139 | Move-Item -Path "$TMP_DOWNLOAD_DIR/$MAVEN_HOME_NAME" -Destination $MAVEN_HOME_PARENT | Out-Null 140 | } catch { 141 | if (! (Test-Path -Path "$MAVEN_HOME" -PathType Container)) { 142 | Write-Error "fail to move MAVEN_HOME" 143 | } 144 | } finally { 145 | try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } 146 | catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } 147 | } 148 | 149 | Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" 150 | -------------------------------------------------------------------------------- /mvnw: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # ---------------------------------------------------------------------------- 3 | # Licensed to the Apache Software Foundation (ASF) under one 4 | # or more contributor license agreements. See the NOTICE file 5 | # distributed with this work for additional information 6 | # regarding copyright ownership. The ASF licenses this file 7 | # to you under the Apache License, Version 2.0 (the 8 | # "License"); you may not use this file except in compliance 9 | # with the License. You may obtain a copy of the License at 10 | # 11 | # http://www.apache.org/licenses/LICENSE-2.0 12 | # 13 | # Unless required by applicable law or agreed to in writing, 14 | # software distributed under the License is distributed on an 15 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 16 | # KIND, either express or implied. See the License for the 17 | # specific language governing permissions and limitations 18 | # under the License. 19 | # ---------------------------------------------------------------------------- 20 | 21 | # ---------------------------------------------------------------------------- 22 | # Apache Maven Wrapper startup batch script, version 3.3.2 23 | # 24 | # Optional ENV vars 25 | # ----------------- 26 | # JAVA_HOME - location of a JDK home dir, required when download maven via java source 27 | # MVNW_REPOURL - repo url base for downloading maven distribution 28 | # MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven 29 | # MVNW_VERBOSE - true: enable verbose log; debug: trace the mvnw script; others: silence the output 30 | # ---------------------------------------------------------------------------- 31 | 32 | set -euf 33 | [ "${MVNW_VERBOSE-}" != debug ] || set -x 34 | 35 | # OS specific support. 36 | native_path() { printf %s\\n "$1"; } 37 | case "$(uname)" in 38 | CYGWIN* | MINGW*) 39 | [ -z "${JAVA_HOME-}" ] || JAVA_HOME="$(cygpath --unix "$JAVA_HOME")" 40 | native_path() { cygpath --path --windows "$1"; } 41 | ;; 42 | esac 43 | 44 | # set JAVACMD and JAVACCMD 45 | set_java_home() { 46 | # For Cygwin and MinGW, ensure paths are in Unix format before anything is touched 47 | if [ -n "${JAVA_HOME-}" ]; then 48 | if [ -x "$JAVA_HOME/jre/sh/java" ]; then 49 | # IBM's JDK on AIX uses strange locations for the executables 50 | JAVACMD="$JAVA_HOME/jre/sh/java" 51 | JAVACCMD="$JAVA_HOME/jre/sh/javac" 52 | else 53 | JAVACMD="$JAVA_HOME/bin/java" 54 | JAVACCMD="$JAVA_HOME/bin/javac" 55 | 56 | if [ ! -x "$JAVACMD" ] || [ ! -x "$JAVACCMD" ]; then 57 | echo "The JAVA_HOME environment variable is not defined correctly, so mvnw cannot run." >&2 58 | echo "JAVA_HOME is set to \"$JAVA_HOME\", but \"\$JAVA_HOME/bin/java\" or \"\$JAVA_HOME/bin/javac\" does not exist." >&2 59 | return 1 60 | fi 61 | fi 62 | else 63 | JAVACMD="$( 64 | 'set' +e 65 | 'unset' -f command 2>/dev/null 66 | 'command' -v java 67 | )" || : 68 | JAVACCMD="$( 69 | 'set' +e 70 | 'unset' -f command 2>/dev/null 71 | 'command' -v javac 72 | )" || : 73 | 74 | if [ ! -x "${JAVACMD-}" ] || [ ! -x "${JAVACCMD-}" ]; then 75 | echo "The java/javac command does not exist in PATH nor is JAVA_HOME set, so mvnw cannot run." >&2 76 | return 1 77 | fi 78 | fi 79 | } 80 | 81 | # hash string like Java String::hashCode 82 | hash_string() { 83 | str="${1:-}" h=0 84 | while [ -n "$str" ]; do 85 | char="${str%"${str#?}"}" 86 | h=$(((h * 31 + $(LC_CTYPE=C printf %d "'$char")) % 4294967296)) 87 | str="${str#?}" 88 | done 89 | printf %x\\n $h 90 | } 91 | 92 | verbose() { :; } 93 | [ "${MVNW_VERBOSE-}" != true ] || verbose() { printf %s\\n "${1-}"; } 94 | 95 | die() { 96 | printf %s\\n "$1" >&2 97 | exit 1 98 | } 99 | 100 | trim() { 101 | # MWRAPPER-139: 102 | # Trims trailing and leading whitespace, carriage returns, tabs, and linefeeds. 103 | # Needed for removing poorly interpreted newline sequences when running in more 104 | # exotic environments such as mingw bash on Windows. 105 | printf "%s" "${1}" | tr -d '[:space:]' 106 | } 107 | 108 | # parse distributionUrl and optional distributionSha256Sum, requires .mvn/wrapper/maven-wrapper.properties 109 | while IFS="=" read -r key value; do 110 | case "${key-}" in 111 | distributionUrl) distributionUrl=$(trim "${value-}") ;; 112 | distributionSha256Sum) distributionSha256Sum=$(trim "${value-}") ;; 113 | esac 114 | done <"${0%/*}/.mvn/wrapper/maven-wrapper.properties" 115 | [ -n "${distributionUrl-}" ] || die "cannot read distributionUrl property in ${0%/*}/.mvn/wrapper/maven-wrapper.properties" 116 | 117 | case "${distributionUrl##*/}" in 118 | maven-mvnd-*bin.*) 119 | MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ 120 | case "${PROCESSOR_ARCHITECTURE-}${PROCESSOR_ARCHITEW6432-}:$(uname -a)" in 121 | *AMD64:CYGWIN* | *AMD64:MINGW*) distributionPlatform=windows-amd64 ;; 122 | :Darwin*x86_64) distributionPlatform=darwin-amd64 ;; 123 | :Darwin*arm64) distributionPlatform=darwin-aarch64 ;; 124 | :Linux*x86_64*) distributionPlatform=linux-amd64 ;; 125 | *) 126 | echo "Cannot detect native platform for mvnd on $(uname)-$(uname -m), use pure java version" >&2 127 | distributionPlatform=linux-amd64 128 | ;; 129 | esac 130 | distributionUrl="${distributionUrl%-bin.*}-$distributionPlatform.zip" 131 | ;; 132 | maven-mvnd-*) MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ ;; 133 | *) MVN_CMD="mvn${0##*/mvnw}" _MVNW_REPO_PATTERN=/org/apache/maven/ ;; 134 | esac 135 | 136 | # apply MVNW_REPOURL and calculate MAVEN_HOME 137 | # maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ 138 | [ -z "${MVNW_REPOURL-}" ] || distributionUrl="$MVNW_REPOURL$_MVNW_REPO_PATTERN${distributionUrl#*"$_MVNW_REPO_PATTERN"}" 139 | distributionUrlName="${distributionUrl##*/}" 140 | distributionUrlNameMain="${distributionUrlName%.*}" 141 | distributionUrlNameMain="${distributionUrlNameMain%-bin}" 142 | MAVEN_USER_HOME="${MAVEN_USER_HOME:-${HOME}/.m2}" 143 | MAVEN_HOME="${MAVEN_USER_HOME}/wrapper/dists/${distributionUrlNameMain-}/$(hash_string "$distributionUrl")" 144 | 145 | exec_maven() { 146 | unset MVNW_VERBOSE MVNW_USERNAME MVNW_PASSWORD MVNW_REPOURL || : 147 | exec "$MAVEN_HOME/bin/$MVN_CMD" "$@" || die "cannot exec $MAVEN_HOME/bin/$MVN_CMD" 148 | } 149 | 150 | if [ -d "$MAVEN_HOME" ]; then 151 | verbose "found existing MAVEN_HOME at $MAVEN_HOME" 152 | exec_maven "$@" 153 | fi 154 | 155 | case "${distributionUrl-}" in 156 | *?-bin.zip | *?maven-mvnd-?*-?*.zip) ;; 157 | *) die "distributionUrl is not valid, must match *-bin.zip or maven-mvnd-*.zip, but found '${distributionUrl-}'" ;; 158 | esac 159 | 160 | # prepare tmp dir 161 | if TMP_DOWNLOAD_DIR="$(mktemp -d)" && [ -d "$TMP_DOWNLOAD_DIR" ]; then 162 | clean() { rm -rf -- "$TMP_DOWNLOAD_DIR"; } 163 | trap clean HUP INT TERM EXIT 164 | else 165 | die "cannot create temp dir" 166 | fi 167 | 168 | mkdir -p -- "${MAVEN_HOME%/*}" 169 | 170 | # Download and Install Apache Maven 171 | verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." 172 | verbose "Downloading from: $distributionUrl" 173 | verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" 174 | 175 | # select .zip or .tar.gz 176 | if ! command -v unzip >/dev/null; then 177 | distributionUrl="${distributionUrl%.zip}.tar.gz" 178 | distributionUrlName="${distributionUrl##*/}" 179 | fi 180 | 181 | # verbose opt 182 | __MVNW_QUIET_WGET=--quiet __MVNW_QUIET_CURL=--silent __MVNW_QUIET_UNZIP=-q __MVNW_QUIET_TAR='' 183 | [ "${MVNW_VERBOSE-}" != true ] || __MVNW_QUIET_WGET='' __MVNW_QUIET_CURL='' __MVNW_QUIET_UNZIP='' __MVNW_QUIET_TAR=v 184 | 185 | # normalize http auth 186 | case "${MVNW_PASSWORD:+has-password}" in 187 | '') MVNW_USERNAME='' MVNW_PASSWORD='' ;; 188 | has-password) [ -n "${MVNW_USERNAME-}" ] || MVNW_USERNAME='' MVNW_PASSWORD='' ;; 189 | esac 190 | 191 | if [ -z "${MVNW_USERNAME-}" ] && command -v wget >/dev/null; then 192 | verbose "Found wget ... using wget" 193 | wget ${__MVNW_QUIET_WGET:+"$__MVNW_QUIET_WGET"} "$distributionUrl" -O "$TMP_DOWNLOAD_DIR/$distributionUrlName" || die "wget: Failed to fetch $distributionUrl" 194 | elif [ -z "${MVNW_USERNAME-}" ] && command -v curl >/dev/null; then 195 | verbose "Found curl ... using curl" 196 | curl ${__MVNW_QUIET_CURL:+"$__MVNW_QUIET_CURL"} -f -L -o "$TMP_DOWNLOAD_DIR/$distributionUrlName" "$distributionUrl" || die "curl: Failed to fetch $distributionUrl" 197 | elif set_java_home; then 198 | verbose "Falling back to use Java to download" 199 | javaSource="$TMP_DOWNLOAD_DIR/Downloader.java" 200 | targetZip="$TMP_DOWNLOAD_DIR/$distributionUrlName" 201 | cat >"$javaSource" <<-END 202 | public class Downloader extends java.net.Authenticator 203 | { 204 | protected java.net.PasswordAuthentication getPasswordAuthentication() 205 | { 206 | return new java.net.PasswordAuthentication( System.getenv( "MVNW_USERNAME" ), System.getenv( "MVNW_PASSWORD" ).toCharArray() ); 207 | } 208 | public static void main( String[] args ) throws Exception 209 | { 210 | setDefault( new Downloader() ); 211 | java.nio.file.Files.copy( java.net.URI.create( args[0] ).toURL().openStream(), java.nio.file.Paths.get( args[1] ).toAbsolutePath().normalize() ); 212 | } 213 | } 214 | END 215 | # For Cygwin/MinGW, switch paths to Windows format before running javac and java 216 | verbose " - Compiling Downloader.java ..." 217 | "$(native_path "$JAVACCMD")" "$(native_path "$javaSource")" || die "Failed to compile Downloader.java" 218 | verbose " - Running Downloader.java ..." 219 | "$(native_path "$JAVACMD")" -cp "$(native_path "$TMP_DOWNLOAD_DIR")" Downloader "$distributionUrl" "$(native_path "$targetZip")" 220 | fi 221 | 222 | # If specified, validate the SHA-256 sum of the Maven distribution zip file 223 | if [ -n "${distributionSha256Sum-}" ]; then 224 | distributionSha256Result=false 225 | if [ "$MVN_CMD" = mvnd.sh ]; then 226 | echo "Checksum validation is not supported for maven-mvnd." >&2 227 | echo "Please disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 228 | exit 1 229 | elif command -v sha256sum >/dev/null; then 230 | if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | sha256sum -c >/dev/null 2>&1; then 231 | distributionSha256Result=true 232 | fi 233 | elif command -v shasum >/dev/null; then 234 | if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | shasum -a 256 -c >/dev/null 2>&1; then 235 | distributionSha256Result=true 236 | fi 237 | else 238 | echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." >&2 239 | echo "Please install either command, or disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 240 | exit 1 241 | fi 242 | if [ $distributionSha256Result = false ]; then 243 | echo "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised." >&2 244 | echo "If you updated your Maven version, you need to update the specified distributionSha256Sum property." >&2 245 | exit 1 246 | fi 247 | fi 248 | 249 | # unzip and move 250 | if command -v unzip >/dev/null; then 251 | unzip ${__MVNW_QUIET_UNZIP:+"$__MVNW_QUIET_UNZIP"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -d "$TMP_DOWNLOAD_DIR" || die "failed to unzip" 252 | else 253 | tar xzf${__MVNW_QUIET_TAR:+"$__MVNW_QUIET_TAR"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -C "$TMP_DOWNLOAD_DIR" || die "failed to untar" 254 | fi 255 | printf %s\\n "$distributionUrl" >"$TMP_DOWNLOAD_DIR/$distributionUrlNameMain/mvnw.url" 256 | mv -- "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" "$MAVEN_HOME" || [ -d "$MAVEN_HOME" ] || die "fail to move MAVEN_HOME" 257 | 258 | clean || : 259 | exec_maven "$@" 260 | -------------------------------------------------------------------------------- /src/main/java/tr/com/rsakin/taskmanagementapp/service/TaskService.java: -------------------------------------------------------------------------------- 1 | package tr.com.rsakin.taskmanagementapp.service; 2 | 3 | import jakarta.transaction.Transactional; 4 | import lombok.RequiredArgsConstructor; 5 | import org.springframework.stereotype.Service; 6 | import tr.com.rsakin.taskmanagementapp.model.dto.response.TaskResponseDTO; 7 | import tr.com.rsakin.taskmanagementapp.model.dto.response.TaskStatistics; 8 | import tr.com.rsakin.taskmanagementapp.model.entity.Task; 9 | import tr.com.rsakin.taskmanagementapp.model.mapper.ManualTaskMapper; 10 | import tr.com.rsakin.taskmanagementapp.model.mapper.TaskResponseMapper; 11 | import tr.com.rsakin.taskmanagementapp.repository.TaskRepository; 12 | 13 | import java.time.Duration; 14 | import java.time.LocalDateTime; 15 | import java.time.format.DateTimeFormatter; 16 | import java.util.*; 17 | import java.util.concurrent.CompletableFuture; 18 | import java.util.concurrent.Executors; 19 | import java.util.function.Consumer; 20 | import java.util.stream.Collectors; 21 | 22 | @Service 23 | @RequiredArgsConstructor 24 | public class TaskService { 25 | 26 | private final TaskRepository taskRepository; 27 | 28 | // Event publishing for task operations (Java 8 functional interfaces) 29 | private final List> taskCreationListeners = new ArrayList<>(); 30 | private final List> taskCompletionListeners = new ArrayList<>(); 31 | 32 | @Transactional 33 | public TaskResponseDTO createTask(String title, String description) { 34 | validateTaskInput(title, description); 35 | 36 | Task task = Task.builder() 37 | .title(title) 38 | .description(description) 39 | .build(); 40 | 41 | Task savedTask = taskRepository.save(task); 42 | 43 | // Notify creation listeners 44 | taskCreationListeners.forEach(listener -> listener.accept(savedTask)); 45 | 46 | return ManualTaskMapper.toDTO(savedTask); 47 | } 48 | 49 | public List getAllTasks() { 50 | return TaskResponseMapper.INSTANCE.toDTOList(taskRepository.findAll()); 51 | } 52 | 53 | public TaskResponseDTO getTaskById(UUID id) { 54 | return taskRepository.findById(id) 55 | .map(TaskResponseMapper.INSTANCE::toDTO) 56 | .orElse(null); 57 | } 58 | 59 | @Transactional 60 | public Task updateTaskStatus(UUID id, Task.TaskStatus newStatus) { 61 | Task task = taskRepository.findById(id) 62 | .orElseThrow(() -> new IllegalArgumentException("Task not found with ID: " + id)); 63 | 64 | if (task.getStatus() == Task.TaskStatus.BLOCKED) 65 | throw new TaskStatusNotAvailableException("Task not found with ID: " + id); 66 | 67 | Task updatedTask = task.updateStatus(newStatus); 68 | Task savedTask = taskRepository.save(updatedTask); 69 | 70 | // Notify completion listeners if task is completed 71 | if (newStatus == Task.TaskStatus.COMPLETED) { 72 | taskCompletionListeners.forEach(listener -> listener.accept(savedTask)); 73 | } 74 | 75 | return savedTask; 76 | } 77 | 78 | @Transactional 79 | public void deleteTask(UUID id) { 80 | taskRepository.deleteById(id); 81 | } 82 | 83 | // Validation 84 | private void validateTaskInput(String title, String description) { 85 | if (title == null || title.trim().isEmpty()) { 86 | throw new IllegalArgumentException("Task title cannot be empty"); 87 | } 88 | 89 | if (description == null) { 90 | throw new IllegalArgumentException("Task description cannot be null"); 91 | } 92 | } 93 | 94 | // Methods using JPA standard repository methods 95 | 96 | public List getTasksByStatus(Task.TaskStatus status) { 97 | return taskRepository.findByStatus(status).stream() 98 | .map(TaskResponseMapper.INSTANCE::toDTO) 99 | .collect(Collectors.toList()); 100 | } 101 | 102 | public Optional findTaskByTitle(String title) { 103 | return taskRepository.findByTitleContainingIgnoreCase(title).stream() 104 | .findFirst() 105 | .map(TaskResponseMapper.INSTANCE::toDTO); 106 | } 107 | 108 | // Methods using JPQL queries 109 | 110 | public List getTasksByPriority(int priorityValue) { 111 | return taskRepository.findTasksByPriorityValue(priorityValue).stream() 112 | .map(TaskResponseMapper.INSTANCE::toDTO) 113 | .collect(Collectors.toList()); 114 | } 115 | 116 | // Methods using native queries 117 | @Transactional 118 | public Map getTaskStatusStatistics() { 119 | List rawStats = taskRepository.getTaskStatusStatistics(); 120 | 121 | Map formattedStats = new HashMap<>(); 122 | formattedStats.put("totalTasks", taskRepository.count()); 123 | 124 | List> statusStats = rawStats.stream() 125 | .map(row -> { 126 | Map stat = new HashMap<>(); 127 | stat.put("status", row[0]); 128 | stat.put("count", row[1]); 129 | stat.put("oldestTask", row[2]); 130 | stat.put("newestTask", row[3]); 131 | return stat; 132 | }) 133 | .collect(Collectors.toList()); 134 | 135 | formattedStats.put("statusBreakdown", statusStats); 136 | return formattedStats; 137 | } 138 | 139 | // Observer pattern methods 140 | public void addTaskCreationListener(Consumer listener) { 141 | taskCreationListeners.add(listener); 142 | } 143 | 144 | public void addTaskCompletionListener(Consumer listener) { 145 | taskCompletionListeners.add(listener); 146 | } 147 | 148 | public TaskStatistics getTaskStatistics() { 149 | long total = taskRepository.count(); 150 | long pending = taskRepository.countByStatus(Task.TaskStatus.PENDING); 151 | long inProgress = taskRepository.countByStatus(Task.TaskStatus.IN_PROGRESS); 152 | long blocked = taskRepository.countByStatus(Task.TaskStatus.BLOCKED); 153 | long completed = taskRepository.countByStatus(Task.TaskStatus.COMPLETED); 154 | 155 | return new TaskStatistics(total, pending, inProgress, blocked, completed); 156 | } 157 | 158 | private long countTasksByStatus(Task.TaskStatus status, List tasks) { 159 | return tasks.stream() 160 | .filter(task -> task.getStatus() == status) 161 | .count(); 162 | } 163 | 164 | // Java 15: Text blocks for complex queries 165 | public String generateTaskReport() { 166 | List tasks = taskRepository.findAll(); 167 | int taskSize = tasks.size(); 168 | return """ 169 | TASK MANAGEMENT REPORT 170 | ---------------------- 171 | Total Tasks: %d 172 | Pending: %d 173 | In Progress: %d 174 | Blocked: %d 175 | Completed: %d 176 | 177 | Last Updated: %s 178 | """.formatted( 179 | taskSize, 180 | countTasksByStatus(Task.TaskStatus.PENDING, tasks), 181 | countTasksByStatus(Task.TaskStatus.IN_PROGRESS, tasks), 182 | countTasksByStatus(Task.TaskStatus.BLOCKED, tasks), 183 | countTasksByStatus(Task.TaskStatus.COMPLETED, tasks), 184 | LocalDateTime.now().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME) 185 | ); 186 | } 187 | 188 | // Java 16: Pattern matching for instanceof 189 | public String getTaskDescription(Object taskIdentifier) { 190 | switch (taskIdentifier) { 191 | case UUID id -> { 192 | Task task = taskRepository.findById(id).get(); // check if task exists 193 | return task != null ? task.getDescription() : "Task not found"; 194 | } 195 | case String title -> { 196 | return findTaskByTitle(title) 197 | .map(TaskResponseDTO::description) 198 | .orElse("Task not found"); 199 | } 200 | default -> { 201 | return "Invalid task identifier"; 202 | } 203 | } 204 | } 205 | 206 | // Java 17: Sealed classes (related to Task.java, shown separately) 207 | 208 | // Java 19: Virtual threads (preview) - Simulating async operations 209 | public CompletableFuture> getTasksAsync() { 210 | return CompletableFuture.supplyAsync(this::getAllTasks, Executors.newVirtualThreadPerTaskExecutor()); 211 | } 212 | 213 | // Java 21: Pattern matching for switch 214 | public String describeTask(Object obj) { 215 | return switch (obj) { 216 | case UUID id -> { 217 | Task task = taskRepository.findById(id).get(); // check if task exists 218 | yield task != null ? "Task: " + task.getTitle() : "Unknown task"; 219 | } 220 | case Task task -> "Task: " + task.getTitle() + " (" + task.getStatus() + ")"; 221 | case String s -> "Search for: " + s; 222 | default -> "Unknown object"; 223 | }; 224 | } 225 | 226 | // Java 21: Record patterns 227 | public record TaskDuration(UUID id, Duration duration) {} 228 | 229 | public List analyzeTaskDurations(List durations) { 230 | return durations.stream() 231 | .map(duration -> switch (duration) { 232 | case TaskDuration(UUID id, Duration d) when d.toHours() < 1 -> 233 | "Task " + id + ": Quick task"; 234 | case TaskDuration(UUID id, Duration d) when d.toHours() < 8 -> 235 | "Task " + id + ": Medium task"; 236 | case TaskDuration(UUID id, Duration d) -> 237 | "Task " + id + ": Long task"; 238 | }) 239 | .toList(); 240 | } 241 | 242 | // Java 22: String Templates (preview) 243 | public String getTaskSummary(UUID id) { 244 | Task task = taskRepository.findById(id).get(); // check if task exists 245 | if (task == null) { 246 | throw new IllegalArgumentException("Task not found with ID: " + id); 247 | } 248 | 249 | // Using StringTemplate.Processor (preview feature) 250 | // Note: In actual code you would use STR."Task: \{task.getTitle()}, Status: \{task.getStatus()}" 251 | // Using traditional string concatenation for compatibility 252 | return "Task: " + task.getTitle() + ", Status: " + task.getStatus(); 253 | } 254 | 255 | // Java 23: Unnamed patterns and variables (preview) 256 | public boolean hasTaskWithStatus(Task.TaskStatus status) { 257 | List tasks = taskRepository.findAll(); 258 | return tasks.stream() 259 | .anyMatch(task -> switch (task) { 260 | // In actual Java 23 code: case Task(_, _, _, status, _) -> true; 261 | case Task t when t.getStatus() == status -> true; 262 | default -> false; 263 | }); 264 | } 265 | 266 | // Java 24: Stream gatherers (preview) 267 | public Map> groupTasksByStatus() { 268 | // In Java 24 this would use the new Stream.gather() API 269 | // For now using traditional groupingBy collector 270 | List tasks = taskRepository.findAll(); 271 | return tasks.stream() 272 | .collect(Collectors.groupingBy( 273 | Task::getStatus, 274 | Collectors.mapping(TaskResponseMapper.INSTANCE::toDTO, Collectors.toList()) 275 | )); 276 | } 277 | 278 | // Use the TaskPriority sealed interface 279 | public Task.Priority getTaskPriorityObject(UUID id) { 280 | Task task = taskRepository.findById(id).get(); // check if task exists 281 | if (task == null) { 282 | throw new IllegalArgumentException("Task not found with ID: " + id); 283 | } 284 | 285 | // Using pattern matching for switch with sealed interface 286 | return switch (task.getStatus()) { 287 | case PENDING, COMPLETED -> new Task.LowPriority(); 288 | case IN_PROGRESS -> new Task.MediumPriority(); 289 | case BLOCKED -> new Task.HighPriority(); 290 | }; 291 | } 292 | 293 | // Method to update task with priority 294 | public Task updateTaskPriority(UUID id, Task.Priority priority) { 295 | Task task = taskRepository.findById(id).get(); // check if task exists 296 | if (task == null) { 297 | throw new IllegalArgumentException("Task not found with ID: " + id); 298 | } 299 | 300 | // We need to update the Task model to include priority 301 | // For now, just returning the task since we can't modify it 302 | // In a real implementation, this would return a new Task with updated priority 303 | return task; 304 | } 305 | 306 | } 307 | -------------------------------------------------------------------------------- /src/main/java/tr/com/rsakin/taskmanagementapp/controller/TaskController.java: -------------------------------------------------------------------------------- 1 | package tr.com.rsakin.taskmanagementapp.controller; 2 | 3 | import io.swagger.v3.oas.annotations.Operation; 4 | import io.swagger.v3.oas.annotations.media.ArraySchema; 5 | import io.swagger.v3.oas.annotations.media.Content; 6 | import io.swagger.v3.oas.annotations.media.Schema; 7 | import io.swagger.v3.oas.annotations.responses.ApiResponse; 8 | import io.swagger.v3.oas.annotations.tags.Tag; 9 | import org.springframework.beans.factory.annotation.Autowired; 10 | import org.springframework.http.HttpStatus; 11 | import org.springframework.http.MediaType; 12 | import org.springframework.http.ResponseEntity; 13 | import org.springframework.web.bind.annotation.*; 14 | import tr.com.rsakin.taskmanagementapp.model.dto.request.PriorityUpdateRequest; 15 | import tr.com.rsakin.taskmanagementapp.model.dto.request.StatusUpdateRequest; 16 | import tr.com.rsakin.taskmanagementapp.model.dto.request.TaskRequest; 17 | import tr.com.rsakin.taskmanagementapp.model.dto.response.TaskResponseDTO; 18 | import tr.com.rsakin.taskmanagementapp.model.dto.response.TaskStatistics; 19 | import tr.com.rsakin.taskmanagementapp.model.entity.Task; 20 | import tr.com.rsakin.taskmanagementapp.service.TaskService; 21 | import tr.com.rsakin.taskmanagementapp.service.TaskStatusNotAvailableException; 22 | 23 | import java.util.List; 24 | import java.util.Map; 25 | import java.util.UUID; 26 | import java.util.concurrent.CompletableFuture; 27 | 28 | @RestController 29 | @RequestMapping("/api/tasks") 30 | @Tag(name = "Task Management", description = "API endpoints for task CRUD operations") 31 | public class TaskController { 32 | 33 | // Composition over inheritance 34 | // - More Flexibility: Inheritance creates a tight coupling between the parent and child classes, whereas composition allows objects to be more modular. 35 | // - Easier Code Maintenance: With composition, you can change behaviors by swapping out components instead of modifying a whole class hierarchy. 36 | // - Avoids Deep Inheritance Trees: Inheritance can lead to complex, hard-to-maintain structures, while composition keeps relationships simpler. 37 | // - Encapsulation & Reusability: 38 | private final TaskService taskService; 39 | 40 | // Clean code: Constructor injection instead of field injection 41 | // Constructor Injection 42 | // Other Injection options: Setter Injection, Field Injection 43 | // Constructor Injection: 44 | // - It allows you to inject dependencies into the constructor of a class. 45 | // - It is useful for dependency injection in Spring, where you can inject dependencies into classes using constructor injection. 46 | // - It is also useful for testability, as it allows you to mock dependencies in unit tests. 47 | @Autowired 48 | // What is annotation : An annotation is a special type of metadata that you can attach to classes, methods, and other elements in a Java program. 49 | // - It is a Spring annotation that tells Spring to automatically inject the taskService field with the appropriate object instance. 50 | public TaskController(TaskService taskService) { 51 | this.taskService = taskService; 52 | } 53 | 54 | // HTTP Methods : GET, POST, PUT, PATCH, DELETE 55 | // HTTP Status Codes : 200, 201, 204, 400, 404, 500 56 | // HTTP Headers : Content-Type, Accept, Authorization 57 | // HTTP Body : JSON, XML 58 | // HTTP Response : JSON, XML 59 | // HyperText : HyperText is a term used to describe the structure and content of documents on the internet. 60 | // What is HTTP : HTTP is a protocol for transferring data over the internet. 61 | // What is REST : REST is an architectural style for building web services. 62 | // What is API : API is a set of endpoints that allow clients to interact with a server. 63 | // What is JSON : JSON is a data format that is used to transfer data over the internet. 64 | // What is XML : XML is a data format that is used to transfer data over the internet. 65 | // Endpoint : A URL that is used to access a resource on a server. 66 | @PostMapping(produces = MediaType.APPLICATION_JSON_VALUE, consumes = MediaType.APPLICATION_JSON_VALUE) 67 | // HTTP Post 68 | // HTTP Status Code : 201 69 | // HTTP Body : JSON 70 | // HTTP Response : JSON 71 | 72 | // @RequestBody : It is a Spring annotation that tells Spring to automatically deserialize the request body into the TaskRequest object. 73 | 74 | @Operation( 75 | summary = "Create a new task", 76 | description = "Creates a new task with the provided title and description", 77 | responses = { 78 | @ApiResponse(responseCode = "201", description = "Task created successfully", 79 | content = @Content(mediaType = "application/json", 80 | schema = @Schema(implementation = TaskResponseDTO.class))), 81 | @ApiResponse(responseCode = "400", description = "Invalid input") 82 | } 83 | ) 84 | public ResponseEntity createTask(@RequestBody TaskRequest request) { 85 | TaskResponseDTO newTask = taskService.createTask(request.getTitle(), request.getDescription()); 86 | return new ResponseEntity<>(newTask, HttpStatus.CREATED); 87 | } 88 | 89 | // CRUD Operations 90 | // Create, Read, Update, Delete 91 | @Operation( 92 | summary = "Get all tasks", 93 | description = "Retrieves a list of all tasks in the system", 94 | responses = { 95 | @ApiResponse(responseCode = "200", description = "List of tasks retrieved successfully", 96 | content = @Content(mediaType = "application/json", 97 | array = @ArraySchema(schema = @Schema(implementation = TaskResponseDTO.class)))) 98 | } 99 | ) 100 | @GetMapping 101 | public ResponseEntity> getAllTasks() { 102 | return ResponseEntity.status(HttpStatus.OK).body(taskService.getAllTasks()); 103 | } 104 | 105 | // What is URL : A URL (Uniform Resource Locator) is a string that specifies the location of a resource on the internet. 106 | // What is URI : A URI (Uniform Resource Identifier) is a string that specifies the location of a resource on the internet. 107 | // URI is more general than URL. 108 | 109 | @Operation( 110 | summary = "Get task by ID", 111 | description = "Retrieves a specific task by its UUID", 112 | responses = { 113 | @ApiResponse(responseCode = "200", description = "Task found", 114 | content = @Content(mediaType = "application/json", 115 | schema = @Schema(implementation = TaskResponseDTO.class))), 116 | @ApiResponse(responseCode = "404", description = "Task not found") 117 | } 118 | ) 119 | @GetMapping("/{id}") 120 | public ResponseEntity getTaskById(@PathVariable UUID id) { 121 | TaskResponseDTO task = taskService.getTaskById(id); 122 | if (task == null) { 123 | return ResponseEntity.notFound().build(); 124 | } 125 | return ResponseEntity.ok(task); 126 | } 127 | 128 | @Operation( 129 | summary = "Update task status", 130 | description = "Updates the status of an existing task", 131 | responses = { 132 | @ApiResponse(responseCode = "200", description = "Task status updated successfully", 133 | content = @Content(mediaType = "application/json", 134 | schema = @Schema(implementation = Task.class))), 135 | @ApiResponse(responseCode = "404", description = "Task not found") 136 | } 137 | ) 138 | @PatchMapping("/{id}/status") 139 | public ResponseEntity updateTaskStatus( 140 | // @PathVariable : It is a Spring annotation that tells Spring to automatically extract the value of the id parameter from the URL. 141 | @PathVariable UUID id, 142 | @RequestBody StatusUpdateRequest request) { 143 | 144 | try { 145 | Task updatedTask = taskService.updateTaskStatus(id, request.status()); 146 | return ResponseEntity.ok(updatedTask); 147 | } catch (IllegalArgumentException e) { 148 | return ResponseEntity.notFound().build(); 149 | } catch (TaskStatusNotAvailableException ex) { 150 | // custom buss logic 151 | return ResponseEntity.status(HttpStatus.NO_CONTENT).build(); 152 | } 153 | } 154 | 155 | @Operation( 156 | summary = "Find task by title", 157 | description = "Retrieves a task with the specified title", 158 | responses = { 159 | @ApiResponse(responseCode = "200", description = "Task found", 160 | content = @Content(mediaType = "application/json", 161 | schema = @Schema(implementation = TaskResponseDTO.class))), 162 | @ApiResponse(responseCode = "404", description = "Task not found") 163 | } 164 | ) 165 | @DeleteMapping("/{id}") 166 | public ResponseEntity deleteTask(@PathVariable UUID id) { 167 | taskService.deleteTask(id); 168 | return ResponseEntity.noContent().build(); 169 | } 170 | 171 | @GetMapping("/status/{status}") 172 | public ResponseEntity> getTasksByStatus(@PathVariable Task.TaskStatus status) { 173 | return ResponseEntity.ok(taskService.getTasksByStatus(status)); 174 | } 175 | 176 | @GetMapping("/title/{title}") 177 | public ResponseEntity findTaskByTitle(@PathVariable String title) { 178 | return taskService.findTaskByTitle(title) 179 | .map(ResponseEntity::ok) 180 | .orElse(ResponseEntity.notFound().build()); 181 | } 182 | 183 | @GetMapping("/statistics") 184 | public ResponseEntity getTaskStatistics() { 185 | return ResponseEntity.ok(taskService.getTaskStatistics()); 186 | } 187 | 188 | @GetMapping("/report") 189 | public ResponseEntity generateTaskReport() { 190 | return ResponseEntity.ok(taskService.generateTaskReport()); 191 | } 192 | 193 | @GetMapping("/async") 194 | public CompletableFuture>> getTasksAsync() { 195 | return taskService.getTasksAsync() 196 | .thenApply(ResponseEntity::ok); 197 | } 198 | 199 | @GetMapping("/group-by-status") 200 | public ResponseEntity>> groupTasksByStatus() { 201 | return ResponseEntity.ok(taskService.groupTasksByStatus()); 202 | } 203 | 204 | @PostMapping("/analyze-durations") 205 | public ResponseEntity> analyzeTaskDurations(@RequestBody List durations) { 206 | return ResponseEntity.ok(taskService.analyzeTaskDurations(durations)); 207 | } 208 | 209 | @GetMapping("/{id}/summary") 210 | public ResponseEntity getTaskSummary(@PathVariable UUID id) { 211 | try { 212 | return ResponseEntity.ok(taskService.getTaskSummary(id)); 213 | } catch (IllegalArgumentException e) { 214 | return ResponseEntity.notFound().build(); 215 | } 216 | } 217 | 218 | @GetMapping("/has-status/{status}") 219 | public ResponseEntity hasTaskWithStatus(@PathVariable Task.TaskStatus status) { 220 | return ResponseEntity.ok(taskService.hasTaskWithStatus(status)); 221 | } 222 | 223 | @GetMapping("/{id}/priority-object") 224 | public ResponseEntity> getTaskPriorityObject(@PathVariable UUID id) { 225 | try { 226 | Task.Priority priority = taskService.getTaskPriorityObject(id); 227 | 228 | // Create a map to represent the priority since we can't directly serialize the interface 229 | Map response = Map.of( 230 | "label", priority.getLabel(), 231 | "value", priority.getValue(), 232 | "type", priority.getClass().getSimpleName() 233 | ); 234 | 235 | return ResponseEntity.ok(response); 236 | } catch (IllegalArgumentException e) { 237 | return ResponseEntity.notFound().build(); 238 | } 239 | } 240 | 241 | @GetMapping("/priority/{value}") 242 | public ResponseEntity> getTasksByPriority(@PathVariable int value) { 243 | if (value < 1 || value > 3) { 244 | return ResponseEntity.badRequest().build(); 245 | } 246 | 247 | return ResponseEntity.ok(taskService.getTasksByPriority(value)); 248 | } 249 | 250 | @PostMapping("/{id}/priority") 251 | public ResponseEntity updateTaskPriority( 252 | @PathVariable UUID id, 253 | @RequestBody PriorityUpdateRequest request) { 254 | try { 255 | // Convert string or int to TaskPriority object based on request 256 | Task.Priority priority = switch(request.value()) { 257 | case 1 -> new Task.LowPriority(); 258 | case 2 -> new Task.MediumPriority(); 259 | case 3 -> new Task.HighPriority(); 260 | default -> throw new IllegalArgumentException("Invalid priority value"); 261 | }; 262 | 263 | Task updatedTask = taskService.updateTaskPriority(id, priority); 264 | return ResponseEntity.ok(updatedTask); 265 | } catch (IllegalArgumentException e) { 266 | return ResponseEntity.badRequest().body(null); 267 | } 268 | } 269 | 270 | @GetMapping("/status-statistics") 271 | public ResponseEntity> getTaskStatusStatistics() { 272 | return ResponseEntity.ok(taskService.getTaskStatusStatistics()); 273 | } 274 | 275 | } 276 | --------------------------------------------------------------------------------