I need to create an endpoint that returns the task list with the start and end date, but tasks that depend on another task to start do not have dates recorded in the database, so I need to create this task list by calculating the start and end based on previous tasks.
Can you tell me how to implement this?
Below is the current answer and the answer I hope to get with your help!
Current response:
[
{
"id": 1,
"name": "Analysis of Requirement",
"isDependent": false,
"dependencies": [],
"durationDays": 30,
"startDate": "2022-08-01",
"endDate": "2022-08-31"
},
{
"id": 2,
"name": "Product Design",
"isDependent": true,
"dependencies": [
{
"id": 1,
"taskDependency": {
"id": 1,
"name": "Analysis of Requirement",
"isDependent": false,
"dependencies": [],
"durationDays": 30,
"startDate": "2022-08-01",
"endDate": "2022-08-31"
}
}
],
"durationDays": 30,
"startDate": null,
"endDate": null
},
{
"id": 3,
"name": "Coding",
"isDependent": true,
"dependencies": [
{
"id": 2,
"taskDependency": {
"id": 2,
"name": "Product Design",
"isDependent": true,
"dependencies": [
{
"id": 1,
"taskDependency": {
"id": 1,
"name": "Analysis of Requirement",
"isDependent": false,
"dependencies": [],
"durationDays": 30,
"startDate": "2022-08-01",
"endDate": "2022-08-31"
}
}
],
"durationDays": 30,
"startDate": null,
"endDate": null
}
}
],
"durationDays": 120,
"startDate": null,
"endDate": null
},
{
"id": 4,
"name": "Testing",
"isDependent": true,
"dependencies": [
{
"id": 3,
"taskDependency": {
"id": 3,
"name": "Coding",
"isDependent": true,
"dependencies": [
{
"id": 2,
"taskDependency": {
"id": 2,
"name": "Product Design",
"isDependent": true,
"dependencies": [
{
"id": 1,
"taskDependency": {
"id": 1,
"name": "Analysis of Requirement",
"isDependent": false,
"dependencies": [],
"durationDays": 30,
"startDate": "2022-08-01",
"endDate": "2022-08-31"
}
}
],
"durationDays": 30,
"startDate": null,
"endDate": null
}
}
],
"durationDays": 120,
"startDate": null,
"endDate": null
}
}
],
"durationDays": 30,
"startDate": null,
"endDate": null
},
{
"id": 5,
"name": "Deployment",
"isDependent": true,
"dependencies": [
{
"id": 4,
"taskDependency": {
"id": 4,
"name": "Testing",
"isDependent": true,
"dependencies": [
{
"id": 3,
"taskDependency": {
"id": 3,
"name": "Coding",
"isDependent": true,
"dependencies": [
{
"id": 2,
"taskDependency": {
"id": 2,
"name": "Product Design",
"isDependent": true,
"dependencies": [
{
"id": 1,
"taskDependency": {
"id": 1,
"name": "Analysis of Requirement",
"isDependent": false,
"dependencies": [],
"durationDays": 30,
"startDate": "2022-08-01",
"endDate": "2022-08-31"
}
}
],
"durationDays": 30,
"startDate": null,
"endDate": null
}
}
],
"durationDays": 120,
"startDate": null,
"endDate": null
}
}
],
"durationDays": 30,
"startDate": null,
"endDate": null
}
}
],
"durationDays": 30,
"startDate": null,
"endDate": null
}
]
Expected response:
[
{
"id": 1,
"name": "Analysis of Requirement",
"isDependent": false,
"dependencies": [],
"durationDays": 30,
"startDate": "2022-08-01",
"endDate": "2022-08-31"
},
{
"id": 2,
"name": "Product Design",
"isDependent": true,
"dependencies": [
{
"id": 1,
"taskDependency": {
"id": 1,
"name": "Analysis of Requirement",
"isDependent": false,
"dependencies": [],
"durationDays": 30,
"startDate": "2022-08-01",
"endDate": "2022-08-31"
}
}
],
"durationDays": 30,
"startDate": "2022-08-31",
"endDate": "2022-09-30"
},
{
"id": 3,
"name": "Coding",
"isDependent": true,
"dependencies": [
{
"id": 2,
"taskDependency": {
"id": 2,
"name": "Product Design",
"isDependent": true,
"dependencies": [
{
"id": 1,
"taskDependency": {
"id": 1,
"name": "Analysis of Requirement",
"isDependent": false,
"dependencies": [],
"durationDays": 30,
"startDate": "2022-08-01",
"endDate": "2022-08-31"
}
}
],
"durationDays": 30,
"startDate": "2022-08-31",
"endDate": "2022-09-30"
}
}
],
"durationDays": 120,
"startDate": "2022-09-30",
"endDate": "2023-01-28"
},
{
"id": 4,
"name": "Testing",
"isDependent": true,
"dependencies": [
{
"id": 3,
"taskDependency": {
"id": 3,
"name": "Coding",
"isDependent": true,
"dependencies": [
{
"id": 2,
"taskDependency": {
"id": 2,
"name": "Product Design",
"isDependent": true,
"dependencies": [
{
"id": 1,
"taskDependency": {
"id": 1,
"name": "Analysis of Requirement",
"isDependent": false,
"dependencies": [],
"durationDays": 30,
"startDate": "2022-08-01",
"endDate": "2022-08-31"
}
}
],
"durationDays": 30,
"startDate": "2022-08-31",
"endDate": "2022-09-30"
}
}
],
"durationDays": 120,
"startDate": "2022-09-30",
"endDate": "2023-01-28"
}
}
],
"durationDays": 30,
"startDate": "2023-01-28",
"endDate": "2023-02-27"
},
{
"id": 5,
"name": "Deployment",
"isDependent": true,
"dependencies": [
{
"id": 4,
"taskDependency": {
"id": 4,
"name": "Testing",
"isDependent": true,
"dependencies": [
{
"id": 3,
"taskDependency": {
"id": 3,
"name": "Coding",
"isDependent": true,
"dependencies": [
{
"id": 2,
"taskDependency": {
"id": 2,
"name": "Product Design",
"isDependent": true,
"dependencies": [
{
"id": 1,
"taskDependency": {
"id": 1,
"name": "Analysis of Requirement",
"isDependent": false,
"dependencies": [],
"durationDays": 30,
"startDate": "2022-08-01",
"endDate": "2022-08-31"
}
}
],
"durationDays": 30,
"startDate": "2022-08-31",
"endDate": "2022-09-30"
}
}
],
"durationDays": 120,
"startDate": "2022-09-30",
"endDate": "2023-01-28"
}
}
],
"durationDays": 30,
"startDate": "2023-01-28",
"endDate": "2023-02-27"
}
}
],
"durationDays": 30,
"startDate": "2023-02-27",
"endDate": "2023-03-29"
}
]
Requirements:
- To simplify the task will have only one previous task as a dependency.
- The finish date of the previous task is the start date of the current task.
- To calculate the dates, you need to go through all the dependent tasks until you reach the task where the
isDependent
field is set tofalse
.
My classes:
MODEL
Task
package com.example.demo.model;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonManagedReference;
import lombok.*;
import javax.persistence.*;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.List;
@Entity
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Table(name = "tasks")
@JsonIgnoreProperties({"hibernateLazyInitializer", "handler"})
public class Task {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private Boolean isDependent;
@EqualsAndHashCode.Exclude
@OneToMany(mappedBy = "task",
cascade = CascadeType.ALL,
orphanRemoval = true,
fetch = FetchType.LAZY)
@JsonManagedReference
private List<TaskDependency> dependencies = new ArrayList<>();
private Integer durationDays;
private LocalDate startDate;
private LocalDate endDate;
}
TaskDependency
package com.example.demo.model;
import com.fasterxml.jackson.annotation.JsonBackReference;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import javax.persistence.*;
@Entity
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Table(name = "task_dependencies")
@JsonIgnoreProperties({"hibernateLazyInitializer", "handler"})
public class TaskDependency {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "task_id", nullable = false
, foreignKey = @ForeignKey(name = "fk_task_dependency_task1"))
@JsonBackReference
private Task task;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "dependency_id", nullable = false
, foreignKey = @ForeignKey(name = "fk_task_dependency_task2"))
private Task taskDependency;
}
DTO
TaskPost
package com.example.demo.dto;
import com.example.demo.model.TaskDependency;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.List;
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class TaskPost {
private String name;
private Boolean isDependent;
private List<TaskDependency> dependencies = new ArrayList<>();
private Integer durationDays;
private LocalDate startDate;
}
MAPPER
TaskMapper
package com.example.demo.mapper;
import com.example.demo.dto.TaskPost;
import com.example.demo.model.Task;
import org.mapstruct.Mapper;
import org.mapstruct.factory.Mappers;
@Mapper(componentModel = "spring")
public abstract class TaskMapper {
public static final TaskMapper INSTANCE = Mappers.getMapper(TaskMapper.class);
public abstract Task toTask(TaskPost taskPost);
}
REPOSITORY
TaskRep
package com.example.demo.repository;
import com.example.demo.model.Task;
import org.springframework.data.jpa.repository.JpaRepository;
public interface TaskRep extends JpaRepository<Task, Long> {
}
SERVICE
TaskService
package com.example.demo.service;
import com.example.demo.dto.TaskPost;
import com.example.demo.mapper.TaskMapper;
import com.example.demo.model.Task;
import com.example.demo.model.TaskDependency;
import com.example.demo.repository.TaskRep;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.List;
@Service
@RequiredArgsConstructor
public class TaskService {
private final TaskRep taskRep;
public List<Task> findAll() {
return taskRep.findAll();
}
@Transactional
public Task save(TaskPost taskPost) {
Task task = TaskMapper.INSTANCE.toTask(taskPost);
List<TaskDependency> taskDependencies = new ArrayList<>(task.getDependencies());
task.setDependencies(taskDependencies);
if (!task.getIsDependent()) {
LocalDate startDate = LocalDate.parse(String.valueOf(task.getStartDate()));
LocalDate endDate = startDate.plusDays(task.getDurationDays());
task.setEndDate(endDate);
}
task.getDependencies().forEach((child) -> child.setTask(task));
return taskRep.save(task);
}
}
CONTROLLER
TaskController
package com.example.demo.controller;
import com.example.demo.dto.TaskPost;
import com.example.demo.model.Task;
import com.example.demo.service.TaskService;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.lang.NonNull;
import org.springframework.web.bind.annotation.*;
import javax.validation.Valid;
import java.util.List;
@RestController
@RequestMapping("api/v1/tasks")
@RequiredArgsConstructor
public class TaskController {
private final TaskService taskService;
@GetMapping
public ResponseEntity<List<Task>> findAll() {
return ResponseEntity.ok(taskService.findAll());
}
@PostMapping
public ResponseEntity<Task> save(@NonNull @Valid @RequestBody TaskPost taskPost) {
return new ResponseEntity<>(taskService.save(taskPost), HttpStatus.CREATED);
}
}
CodePudding user response:
I would have an algorithm like this:
- read in only the root level of tasks, with only the IDs of the dependent tasks. The other information of the nested tasks looks pretty redundant to me. Read them into a map with the task ID as key, and task as value.
- process the entryset of the map once to set the predecessor of each task once as object reference. In this step put all tasks into a new Queue, because after that step you won't need the map anylonger, but might need to process entries multiple times. Or you stick to the entryset for further processing.
- now process the queue of all tasks: take the first entry, see if it has a start time set, and a predecessor.
- If it has set none of them, report an error (you can't calculate times then).
- If it has no predecessor, work done. Put to a "processed" set for output.
- If it has a start time, work done. Put to a "processed" set for output.
- If none of the above, see if the predecessor has an end time set.
- If yes, calculuate your own start and end time and put to a "processed" set for output.
- If no, add entry to the queue again.
Process queue entries as long as the queue is not empty. This should save you from doing recursive processing.
For the output, you might need to recreate the model, or if you designed it carefully you just can serialize the "processed" set.
UPDATE: short demo code per request - only the algo, not the handling of requests or JSON parsing or writing.
Please note that just for the sake of brevity I skipped the getters and setters on the Task
class, I do not recommend to do so in "real" code.
Task.class:
package examples.stackoverflow.q72967644;
import java.time.LocalDate;
import java.util.Objects;
class Task {
final Long id;
final String name;
Long dependsOn;
final Integer durationDays;
LocalDate startDate;
Task depends;
@Override
public String toString() {
return "Task{"
"id=" id
", name='" name '\''
", dependsOn=" dependsOn
", durationDays=" durationDays
", startDate=" startDate
'}';
}
public Task(Long id, String name, Integer durationDays, Long dependsOn) {
this.id = id;
this.name = name;
this.durationDays = durationDays;
this.dependsOn = dependsOn;
}
public Task(Long id, String name, Integer durationDays, LocalDate startDate) {
this.id = id;
this.name = name;
this.durationDays = durationDays;
this.startDate = startDate;
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
Task task = (Task) o;
return Objects.equals(id, task.id);
}
@Override
public int hashCode() {
return id != null ? id.hashCode() : 0;
}
}
Q72967644.class:
package examples.stackoverflow.q72967644;
import java.time.LocalDate;
import java.util.*;
import java.util.stream.Collectors;
public class Q72967644 {
public static void main(String[] args) {
Q72967644 tests = new Q72967644();
List<Task> tasks = tests.init();
tests.calculateDates(tasks);
tasks.forEach(t->System.out.println(t));
}
private List<Task> init() {
List<Task> tasks = new ArrayList<>(5);
tasks.add(new Task(1L, "Analysis of Requirement", 30, LocalDate.of(2022, 8, 1)));
tasks.add(new Task(2L, "Product Design", 30, 1L));
tasks.add(new Task(3L, "Coding", 120, 2L));
tasks.add(new Task(4L, "Testing", 30, 3L));
tasks.add(new Task(5L, "Deployment", 30, 4L));
return tasks;
}
private void calculateDates(List<Task> tasks) {
// Step 1: read into a map
Map<Long, Task> taskById = tasks.stream().collect(Collectors.toMap(t->t.id, t->t));
// Step 2: resolve dependencies
Queue<Task> processing = new LinkedList<>(tasks);
processing.forEach(task -> task.depends = taskById.get(task.dependsOn));
// Step 3: calculate
while (!processing.isEmpty()) {
Task task = processing.poll();
if (task.startDate == null) {
final Task predecessor = task.depends;
if (predecessor == null) {
throw new IllegalStateException("task found with no start time and no predecessor");
}
if (predecessor.startDate == null) {
processing.add(task);
} else {
task.startDate = predecessor.startDate.plusDays(predecessor.durationDays);
}
}
}
}
}