Skip to content

Commit 6aeafdc

Browse files
committed
#initial-commit
1 parent 8afdffb commit 6aeafdc

File tree

10 files changed

+293
-143
lines changed

10 files changed

+293
-143
lines changed

README.md

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -16,16 +16,16 @@ It is designed to be extended and adapted for any entity that implements the `IN
1616

1717
### Core Components
1818

19-
- **`INestedSetNode<ID>`**
19+
- **`INestedSetNode<ID, T extends INestedSetNode<ID, T>>`**
2020
Interface that defines the structure of a nested set node (left, right, parent).
2121

2222
- **`INestedSetNodeResponse<ID>`**
2323
Interface for representing nodes with children, used for building hierarchical responses.
2424

25-
- **`JpaNestedSetRepository<T, ID>`**
25+
- **`JpaNestedSetRepository<T extends INestedSetNode<ID, T>, ID> extends JpaRepository<T, ID>`**
2626
Base JPA repository interface with custom queries for nested set operations (e.g. find ancestors, descendants, siblings).
2727

28-
- **`AbstractNestedSetService<T, ID>`**
28+
- **` AbstractNestedSetService<T extends INestedSetNode<ID, T>, ID>`**
2929
Abstract service class that implements common logic for creating, moving, and restructuring nodes in a nested set tree.
3030

3131
---
@@ -48,32 +48,32 @@ Add the following dependency to your `pom.xml` file:
4848
<dependency>
4949
<groupId>com.mewebstudio</groupId>
5050
<artifactId>spring-boot-jpa-nested-set</artifactId>
51-
<version>0.1.1</version>
51+
<version>0.1.2</version>
5252
</dependency>
5353
```
5454
#### for gradle users
5555
Add the following dependency to your `build.gradle` file:
5656
```groovy
57-
implementation 'com.mewebstudio:spring-boot-jpa-nested-set:0.1.1'
57+
implementation 'com.mewebstudio:spring-boot-jpa-nested-set:0.1.2'
5858
```
5959

6060
## 🚀 Usage
6161

62-
### 1. Example entity class `INestedSetNode<ID>`
62+
### 1. Example entity class `INestedSetNode<ID, T extends INestedSetNode<ID, T>>`
6363
```java
6464
@Entity
65-
public class Category implements INestedSetNode<String> {
65+
public class Category extends AbstractBaseEntity implements INestedSetNode<String, Category> {
6666
// implement getId, getLeft, getRight, getParent, etc.
6767
}
6868
```
6969

70-
### 2. Example repository interface `JpaNestedSetRepository<Category, String>`
70+
### 2. Example repository interface `JpaNestedSetRepository<T extends INestedSetNode<ID, T>, ID> extends JpaRepository<T, ID>`
7171
```java
7272
public interface CategoryRepository extends JpaNestedSetRepository<Category, String> {
7373
}
7474
```
7575

76-
### 3. Example service class `AbstractNestedSetService<Category, String>`
76+
### 3. Example service class `AbstractNestedSetService<T extends INestedSetNode<ID, T>, ID>`
7777
```java
7878
// Example service class
7979
@Service
Lines changed: 133 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,22 @@
11
package com.mewebstudio.springboot.jpa.nestedset;
22

3+
import jakarta.persistence.EntityNotFoundException;
34
import jakarta.transaction.Transactional;
4-
import org.apache.commons.lang3.tuple.Pair;
55

66
import java.util.ArrayList;
7+
import java.util.Collections;
78
import java.util.Comparator;
89
import java.util.List;
910
import java.util.Optional;
1011
import java.util.stream.Collectors;
11-
import java.util.stream.Stream;
1212

1313
/**
1414
* Abstract service class for managing nested set trees.
1515
*
1616
* @param <T> The type of the nested set node.
1717
* @param <ID> The type of the identifier for the nested set node.
1818
*/
19-
public abstract class AbstractNestedSetService<T extends INestedSetNode<ID>, ID> {
19+
public abstract class AbstractNestedSetService<T extends INestedSetNode<ID, T>, ID> {
2020
private static final int TEMP_OFFSET = Integer.MAX_VALUE;
2121

2222
/**
@@ -79,117 +79,159 @@ public T moveDown(T node) {
7979
* Creates a new node in the nested set tree.
8080
*
8181
* @param allNodes The list of all nodes in the tree.
82-
* @param parent The parent node under which the new node will be created.
83-
* @return A pair of integers representing the left and right values of the new node.
82+
* @param node T The new node to be created.
83+
* @return T The created node.
8484
*/
8585
@Transactional
86-
public Pair<Integer, Integer> createNode(List<T> allNodes, T parent) {
86+
protected T createNode(List<T> allNodes, T node) {
87+
Pair<Integer, Integer> gap = getNodeGap(allNodes, node.getParent());
88+
node.setLeft(gap.first());
89+
node.setRight(gap.second());
90+
return repository.save(node);
91+
}
92+
93+
/**
94+
* Creates a new node in the nested set tree.
95+
*
96+
* @param node T The new node to be created.
97+
* @return T The created node.
98+
*/
99+
@Transactional
100+
protected T createNode(T node) {
101+
return createNode(repository.findAllOrderedByLeft(), node);
102+
}
103+
104+
/**
105+
* Get the gap for inserting a new node in the nested set tree.
106+
*
107+
* @param allNodes The list of all nodes in the tree.
108+
* @param parent T The parent node under which the new node will be created.
109+
* @return A pair of integers representing the left and right values for the new node.
110+
*/
111+
@Transactional
112+
protected Pair<Integer, Integer> getNodeGap(List<T> allNodes, INestedSetNode<ID, T> parent) {
87113
if (parent == null) {
88114
int maxRight = allNodes.stream()
89115
.mapToInt(T::getRight)
90116
.max()
91117
.orElse(0);
92-
return Pair.of(maxRight + 1, maxRight + 2);
118+
return new Pair<>(maxRight + 1, maxRight + 2);
93119
} else {
94120
ID parentId = parent.getId();
95-
if (parentId == null) {
96-
throw new IllegalArgumentException("Parent ID cannot be null");
121+
T parentNode = repository.lockNode(parentId).orElseThrow(() ->
122+
new EntityNotFoundException("Parent node not found with id: " + parentId));
123+
124+
int insertAt = parentNode.getRight();
125+
List<T> shiftedNodes = repository.findNodesToShift(insertAt);
126+
for (T node : shiftedNodes) {
127+
if (node.getLeft() >= insertAt) node.setLeft(node.getLeft() + 2);
128+
if (node.getRight() >= insertAt) node.setRight(node.getRight() + 2);
97129
}
98130

99-
T parentFromDb = repository.lockNode(parentId)
100-
.orElseThrow(() -> new IllegalArgumentException("Parent not found: " + parentId));
131+
parentNode.setRight(parentNode.getRight() + 2);
132+
saveAllNodes(mergeList(Collections.singletonList(parentNode), shiftedNodes));
101133

102-
int insertPosition = parentFromDb.getRight();
103-
List<T> nodesToShift = repository.findNodesToShift(insertPosition);
134+
return new Pair<>(insertAt, insertAt + 1);
135+
}
136+
}
104137

105-
for (T node : nodesToShift) {
106-
if (node.getLeft() >= insertPosition) node.setLeft(node.getLeft() + 2);
107-
if (node.getRight() >= insertPosition) node.setRight(node.getRight() + 2);
108-
}
138+
/**
139+
* Update a node in the nested set tree.
140+
*
141+
* @param node T The node to be updated.
142+
* @param newParent T The new parent node under which the node will be moved.
143+
* @return T The updated node.
144+
*/
145+
@Transactional
146+
protected T updateNode(T node, T newParent) {
147+
if (newParent != null && isDescendant(node, newParent)) {
148+
throw new IllegalArgumentException("Cannot move category under its own descendant");
149+
}
109150

110-
parentFromDb.setRight(parentFromDb.getRight() + 2);
151+
int distance = node.getRight() - node.getLeft() + 1;
152+
List<T> allCategories = repository.findAllOrderedByLeft();
153+
closeGapInTree(node, distance, allCategories);
111154

112-
List<T> combinedList = Stream.concat(Stream.of(parentFromDb), nodesToShift.stream())
113-
.collect(Collectors.toList());
114-
saveAllNodes(combinedList);
155+
Pair<Integer, Integer> nodePositions = getNodeGap(allCategories, newParent);
156+
node.setParent(newParent);
157+
node.setLeft(nodePositions.first());
158+
node.setRight(nodePositions.second());
115159

116-
return Pair.of(insertPosition, insertPosition + 1);
117-
}
160+
return repository.save(node);
161+
}
162+
163+
/**
164+
* Deletes a node from the nested set tree.
165+
*
166+
* @param node T The node to be deleted.
167+
*/
168+
@Transactional
169+
protected void deleteNode(T node) {
170+
int width = node.getRight() - node.getLeft() + 1;
171+
List<T> subtree = repository.findSubtree(node.getLeft(), node.getRight());
172+
repository.deleteAll(subtree);
173+
closeGapInTree(node, width, repository.findAllOrderedByLeft());
118174
}
119175

120176
/**
121177
* Closes the gap in the tree after a node is deleted.
122178
*
123-
* @param entity The node that was deleted.
124-
* @param width The width of the gap to be closed.
125-
* @param allNodes The list of all nodes in the tree.
179+
* @param entity T The node that was deleted.
180+
* @param width int The width of the gap to be closed.
181+
* @param allNodes List The list of all nodes in the tree.
126182
*/
127183
protected void closeGapInTree(T entity, int width, List<T> allNodes) {
128184
List<T> updatedNodes = allNodes.stream()
129-
.filter(node -> node.getLeft() > entity.getRight())
130-
.peek(node -> {
131-
node.setLeft(node.getLeft() - width);
132-
node.setRight(node.getRight() - width);
185+
.filter(n -> n.getLeft() > entity.getRight())
186+
.peek(n -> {
187+
n.setLeft(n.getLeft() - width);
188+
n.setRight(n.getRight() - width);
133189
})
134190
.collect(Collectors.toList());
135191

136192
updatedNodes.addAll(allNodes.stream()
137-
.filter(node -> node.getRight() > entity.getRight() && node.getLeft() < entity.getRight())
138-
.peek(node -> node.setRight(node.getRight() - width))
193+
.filter(n -> n.getRight() > entity.getRight() && n.getLeft() < entity.getRight())
194+
.peek(n -> n.setRight(n.getRight() - width))
139195
.toList());
140196
}
141197

142198
/**
143199
* Move a node in the tree.
144200
*
145-
* @param node The node to be moved.
146-
* @param direction The direction in which the node will be moved (up or down).
201+
* @param node T The node to be moved.
202+
* @param direction MoveNodeDirection The direction in which the node will be moved (up or down).
147203
* @return T The updated node.
148204
*/
149205
@Transactional
150206
protected T moveNode(T node, MoveNodeDirection direction) {
151207
ID parentId = node.getParent() != null ? node.getParent().getId() : null;
208+
Optional<T> sibling = direction == MoveNodeDirection.UP ?
209+
repository.findPrevSibling(parentId, node.getLeft()) :
210+
repository.findNextSibling(parentId, node.getRight());
152211

153-
Optional<T> siblingOpt;
154-
if (direction == MoveNodeDirection.UP) {
155-
siblingOpt = repository.findPrevSibling(parentId, node.getLeft());
156-
} else {
157-
siblingOpt = repository.findNextSibling(parentId, node.getRight());
158-
}
159-
160-
if (siblingOpt.isEmpty()) return node;
161-
162-
T sibling = siblingOpt.get();
212+
if (sibling.isEmpty()) return node;
163213

164214
int nodeWidth = node.getRight() - node.getLeft() + 1;
165-
int siblingWidth = sibling.getRight() - sibling.getLeft() + 1;
215+
int siblingWidth = sibling.get().getRight() - sibling.get().getLeft() + 1;
166216

167217
List<T> nodeSubtree = repository.findSubtree(node.getLeft(), node.getRight());
168-
List<T> siblingSubtree = repository.findSubtree(sibling.getLeft(), sibling.getRight());
218+
List<T> siblingSubtree = repository.findSubtree(sibling.get().getLeft(), sibling.get().getRight());
169219

170-
for (T n : nodeSubtree) {
220+
nodeSubtree.forEach(n -> {
171221
n.setLeft(n.getLeft() + TEMP_OFFSET);
172222
n.setRight(n.getRight() + TEMP_OFFSET);
173-
}
223+
});
174224

175-
for (T s : siblingSubtree) {
176-
if (direction == MoveNodeDirection.UP) {
177-
s.setLeft(s.getLeft() + nodeWidth);
178-
s.setRight(s.getRight() + nodeWidth);
179-
} else {
180-
s.setLeft(s.getLeft() - nodeWidth);
181-
s.setRight(s.getRight() - nodeWidth);
182-
}
225+
for (T n : siblingSubtree) {
226+
int offset = direction == MoveNodeDirection.UP ? nodeWidth : -nodeWidth;
227+
n.setLeft(n.getLeft() + offset);
228+
n.setRight(n.getRight() + offset);
183229
}
184230

185231
for (T n : nodeSubtree) {
186-
if (direction == MoveNodeDirection.UP) {
187-
n.setLeft(n.getLeft() - TEMP_OFFSET - siblingWidth);
188-
n.setRight(n.getRight() - TEMP_OFFSET - siblingWidth);
189-
} else {
190-
n.setLeft(n.getLeft() - TEMP_OFFSET + siblingWidth);
191-
n.setRight(n.getRight() - TEMP_OFFSET + siblingWidth);
192-
}
232+
int offset = direction == MoveNodeDirection.UP ? -TEMP_OFFSET - siblingWidth : -TEMP_OFFSET + siblingWidth;
233+
n.setLeft(n.getLeft() + offset);
234+
n.setRight(n.getRight() + offset);
193235
}
194236

195237
List<T> all = new ArrayList<>();
@@ -203,8 +245,8 @@ protected T moveNode(T node, MoveNodeDirection direction) {
203245
/**
204246
* Check if a node is a descendant of another node.
205247
*
206-
* @param ancestor The potential ancestor node.
207-
* @param descendant The potential descendant node.
248+
* @param ancestor T The potential ancestor node.
249+
* @param descendant T The potential descendant node.
208250
* @return True if the descendant is a child of the ancestor, false otherwise.
209251
*/
210252
protected boolean isDescendant(T ancestor, T descendant) {
@@ -214,32 +256,30 @@ protected boolean isDescendant(T ancestor, T descendant) {
214256
/**
215257
* Rebuild the tree structure.
216258
*
217-
* @param parent The parent node of the current node being processed.
218-
* @param allNodes The list of all nodes in the tree.
219-
* @param currentLeft The current left value of the node being processed.
220-
* @return The right value of the node being processed.
259+
* @param parent T The parent node of the current node being processed.
260+
* @param allNodes List The list of all nodes in the tree.
261+
* @param currentLeft Int The current left value of the node being processed.
262+
* @return Int The right value of the node being processed.
221263
*/
222264
@Transactional
223265
protected int rebuildTree(T parent, List<T> allNodes, int currentLeft) {
224266
int left = currentLeft;
225-
ID parentId = parent == null ? null : parent.getId();
267+
ID parentId = parent != null ? parent.getId() : null;
226268

227269
List<T> children = allNodes.stream()
228270
.filter(node -> {
229-
if (parentId == null) {
230-
return node.getParent() == null;
231-
}
271+
if (parentId == null) return node.getParent() == null;
232272
return node.getParent() != null && parentId.equals(node.getParent().getId());
233273
})
234-
.sorted(Comparator.comparingInt(INestedSetNode::getLeft))
274+
.sorted(Comparator.comparingInt(T::getLeft))
235275
.toList();
236276

237277
for (T child : children) {
238278
int childLeft = left + 1;
239279
int right = rebuildTree(child, allNodes, childLeft);
240280
child.setLeft(childLeft);
241281
child.setRight(right);
242-
saveAllNodes(List.of(child));
282+
saveAllNodes(Collections.singletonList(child));
243283
left = right;
244284
}
245285

@@ -249,9 +289,9 @@ protected int rebuildTree(T parent, List<T> allNodes, int currentLeft) {
249289
/**
250290
* Rebuild the tree structure starting from the root node.
251291
*
252-
* @param parent The root node of the tree.
253-
* @param allNodes The list of all nodes in the tree.
254-
* @return The right value of the root node.
292+
* @param parent T The root node of the tree.
293+
* @param allNodes List The list of all nodes in the tree.
294+
* @return int The right value of the root node.
255295
*/
256296
@Transactional
257297
protected int rebuildTree(T parent, List<T> allNodes) {
@@ -261,12 +301,25 @@ protected int rebuildTree(T parent, List<T> allNodes) {
261301
/**
262302
* Save all nodes in the tree.
263303
*
264-
* @param nodes The list of nodes to be saved.
265-
* @return The list of saved nodes.
304+
* @param nodes List The list of nodes to be saved.
305+
* @return List The list of saved nodes.
266306
*/
267307
protected List<T> saveAllNodes(List<T> nodes) {
268308
List<T> savedNodes = repository.saveAll(nodes);
269309
repository.flush();
270310
return savedNodes;
271311
}
312+
313+
/**
314+
* Merge two lists into one.
315+
*
316+
* @param list1 List The first list.
317+
* @param list2 List The second list.
318+
* @return List The merged list.
319+
*/
320+
private List<T> mergeList(List<T> list1, List<T> list2) {
321+
List<T> merged = new ArrayList<>(list1);
322+
merged.addAll(list2);
323+
return merged;
324+
}
272325
}

0 commit comments

Comments
 (0)