1 package fr.tiogars.chat.domains.taxonomy;
2
3 import java.util.HashSet;
4 import java.util.List;
5
6 import fr.tiogars.chat.domains.tag.entities.TagEntity;
7 import fr.tiogars.chat.domains.tag.models.CreateTagRequest;
8 import fr.tiogars.chat.domains.tag.models.UpdateTagRequest;
9 import fr.tiogars.chat.domains.tag.models.Tag;
10 import fr.tiogars.chat.domains.tag.repositories.TagRepository;
11 import fr.tiogars.chat.domains.nomenclature.entities.NomenclatureEntity;
12 import fr.tiogars.chat.domains.nomenclature.models.CreateNomenclatureRequest;
13 import fr.tiogars.chat.domains.nomenclature.models.UpdateNomenclatureRequest;
14 import fr.tiogars.chat.domains.nomenclature.models.Nomenclature;
15 import fr.tiogars.chat.domains.nomenclature.repositories.NomenclatureRepository;
16 import org.springframework.http.HttpStatus;
17 import org.springframework.stereotype.Service;
18 import org.springframework.transaction.annotation.Transactional;
19 import org.springframework.web.server.ResponseStatusException;
20
21 @Service
22 @Transactional
23 public class TaxonomyService {
24 private final TagRepository tags;
25 private final NomenclatureRepository nomenclatures;
26
27 public TaxonomyService(TagRepository tags, NomenclatureRepository nomenclatures) {
28 this.tags = tags;
29 this.nomenclatures = nomenclatures;
30 }
31
32 public List<Tag> tags() {
33 return tags.findAllByOrderByCodeAsc().stream().map(Tag::from).toList();
34 }
35
36 public Tag tag(Long id) {
37 return Tag.from(tags.findById(id).orElseThrow(() -> missing("Tag")));
38 }
39
40 public Tag createTag(CreateTagRequest request) {
41 if (tags.existsByCode(request.code())) {
42 throw conflict("Tag code already exists.");
43 }
44 return Tag.from(tags.saveAndFlush(new TagEntity(request.code(), request.label(), request.description())));
45 }
46
47 public Tag updateTag(Long id, UpdateTagRequest request) {
48 TagEntity entity = tags.lockById(id).orElseThrow(() -> missing("Tag"));
49 entity.update(request.label(), request.description());
50 return Tag.from(tags.saveAndFlush(entity));
51 }
52
53 public void deleteTag(Long id) {
54 TagEntity entity = tags.lockById(id).orElseThrow(() -> missing("Tag"));
55 if (tags.countTaskReferences(id) > 0) {
56 throw conflict("Tag is referenced by a task.");
57 }
58 tags.delete(entity);
59 tags.flush();
60 }
61
62 public List<Nomenclature> nomenclatures() {
63 return nomenclatures.findAllByOrderByCodeAsc().stream().map(Nomenclature::from).toList();
64 }
65
66 public Nomenclature nomenclature(Long id) {
67 return Nomenclature.from(nomenclatures.findById(id).orElseThrow(() -> missing("Nomenclature")));
68 }
69
70 public Nomenclature createNomenclature(CreateNomenclatureRequest request) {
71 nomenclatures.lockForest();
72 if (nomenclatures.existsByCode(request.code())) {
73 throw conflict("Nomenclature code already exists.");
74 }
75 NomenclatureEntity entity = new NomenclatureEntity(request.code(), request.label(), request.description());
76 entity.setParent(parent(null, request.parentId()));
77 return Nomenclature.from(nomenclatures.saveAndFlush(entity));
78 }
79
80 public Nomenclature updateNomenclature(Long id, UpdateNomenclatureRequest request) {
81 nomenclatures.lockForest();
82 NomenclatureEntity entity = nomenclatures.findById(id).orElseThrow(() -> missing("Nomenclature"));
83 entity.setParent(parent(id, request.parentId()));
84 entity.update(request.label(), request.description());
85 return Nomenclature.from(nomenclatures.saveAndFlush(entity));
86 }
87
88 public void deleteNomenclature(Long id) {
89 nomenclatures.lockForest();
90 NomenclatureEntity entity = nomenclatures.findById(id).orElseThrow(() -> missing("Nomenclature"));
91 if (nomenclatures.existsByParentId(id) || nomenclatures.countTaskReferences(id) > 0) {
92 throw conflict("Nomenclature is referenced by a task or child.");
93 }
94 nomenclatures.delete(entity);
95 nomenclatures.flush();
96 }
97
98 private NomenclatureEntity parent(Long id, Long parentId) {
99 if (parentId == null) {
100 return null;
101 }
102 NomenclatureEntity parent = nomenclatures.findById(parentId)
103 .orElseThrow(() -> invalid("Parent nomenclature does not exist."));
104 HashSet<Long> visited = new HashSet<>();
105 for (NomenclatureEntity ancestor = parent; ancestor != null; ancestor = ancestor.getParent()) {
106 if (ancestor.getId().equals(id) || !visited.add(ancestor.getId())) {
107 throw invalid("Nomenclature parent would create a cycle.");
108 }
109 }
110 return parent;
111 }
112
113 private static ResponseStatusException missing(String type) {
114 return new ResponseStatusException(HttpStatus.NOT_FOUND, type + " not found.");
115 }
116
117 private static ResponseStatusException invalid(String message) {
118 return new ResponseStatusException(HttpStatus.BAD_REQUEST, message);
119 }
120
121 private static ResponseStatusException conflict(String message) {
122 return new ResponseStatusException(HttpStatus.CONFLICT, message);
123 }
124 }