1
1
package com .mewebstudio .springboot .jpa .nestedset ;
2
2
3
+ import jakarta .persistence .EntityNotFoundException ;
3
4
import jakarta .transaction .Transactional ;
4
- import org .apache .commons .lang3 .tuple .Pair ;
5
5
6
6
import java .util .ArrayList ;
7
+ import java .util .Collections ;
7
8
import java .util .Comparator ;
8
9
import java .util .List ;
9
10
import java .util .Optional ;
10
11
import java .util .stream .Collectors ;
11
- import java .util .stream .Stream ;
12
12
13
13
/**
14
14
* Abstract service class for managing nested set trees.
15
15
*
16
16
* @param <T> The type of the nested set node.
17
17
* @param <ID> The type of the identifier for the nested set node.
18
18
*/
19
- public abstract class AbstractNestedSetService <T extends INestedSetNode <ID >, ID > {
19
+ public abstract class AbstractNestedSetService <T extends INestedSetNode <ID , T >, ID > {
20
20
private static final int TEMP_OFFSET = Integer .MAX_VALUE ;
21
21
22
22
/**
@@ -79,117 +79,159 @@ public T moveDown(T node) {
79
79
* Creates a new node in the nested set tree.
80
80
*
81
81
* @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.
84
84
*/
85
85
@ 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 ) {
87
113
if (parent == null ) {
88
114
int maxRight = allNodes .stream ()
89
115
.mapToInt (T ::getRight )
90
116
.max ()
91
117
.orElse (0 );
92
- return Pair . of (maxRight + 1 , maxRight + 2 );
118
+ return new Pair <> (maxRight + 1 , maxRight + 2 );
93
119
} else {
94
120
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 );
97
129
}
98
130
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 ));
101
133
102
- int insertPosition = parentFromDb .getRight ();
103
- List <T > nodesToShift = repository .findNodesToShift (insertPosition );
134
+ return new Pair <>(insertAt , insertAt + 1 );
135
+ }
136
+ }
104
137
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
+ }
109
150
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 );
111
154
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 ());
115
159
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 ());
118
174
}
119
175
120
176
/**
121
177
* Closes the gap in the tree after a node is deleted.
122
178
*
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.
126
182
*/
127
183
protected void closeGapInTree (T entity , int width , List <T > allNodes ) {
128
184
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 );
133
189
})
134
190
.collect (Collectors .toList ());
135
191
136
192
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 ))
139
195
.toList ());
140
196
}
141
197
142
198
/**
143
199
* Move a node in the tree.
144
200
*
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).
147
203
* @return T The updated node.
148
204
*/
149
205
@ Transactional
150
206
protected T moveNode (T node , MoveNodeDirection direction ) {
151
207
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 ());
152
211
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 ;
163
213
164
214
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 ;
166
216
167
217
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 ());
169
219
170
- for ( T n : nodeSubtree ) {
220
+ nodeSubtree . forEach ( n -> {
171
221
n .setLeft (n .getLeft () + TEMP_OFFSET );
172
222
n .setRight (n .getRight () + TEMP_OFFSET );
173
- }
223
+ });
174
224
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 );
183
229
}
184
230
185
231
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 );
193
235
}
194
236
195
237
List <T > all = new ArrayList <>();
@@ -203,8 +245,8 @@ protected T moveNode(T node, MoveNodeDirection direction) {
203
245
/**
204
246
* Check if a node is a descendant of another node.
205
247
*
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.
208
250
* @return True if the descendant is a child of the ancestor, false otherwise.
209
251
*/
210
252
protected boolean isDescendant (T ancestor , T descendant ) {
@@ -214,32 +256,30 @@ protected boolean isDescendant(T ancestor, T descendant) {
214
256
/**
215
257
* Rebuild the tree structure.
216
258
*
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.
221
263
*/
222
264
@ Transactional
223
265
protected int rebuildTree (T parent , List <T > allNodes , int currentLeft ) {
224
266
int left = currentLeft ;
225
- ID parentId = parent == null ? null : parent .getId ();
267
+ ID parentId = parent != null ? parent .getId () : null ;
226
268
227
269
List <T > children = allNodes .stream ()
228
270
.filter (node -> {
229
- if (parentId == null ) {
230
- return node .getParent () == null ;
231
- }
271
+ if (parentId == null ) return node .getParent () == null ;
232
272
return node .getParent () != null && parentId .equals (node .getParent ().getId ());
233
273
})
234
- .sorted (Comparator .comparingInt (INestedSetNode ::getLeft ))
274
+ .sorted (Comparator .comparingInt (T ::getLeft ))
235
275
.toList ();
236
276
237
277
for (T child : children ) {
238
278
int childLeft = left + 1 ;
239
279
int right = rebuildTree (child , allNodes , childLeft );
240
280
child .setLeft (childLeft );
241
281
child .setRight (right );
242
- saveAllNodes (List . of (child ));
282
+ saveAllNodes (Collections . singletonList (child ));
243
283
left = right ;
244
284
}
245
285
@@ -249,9 +289,9 @@ protected int rebuildTree(T parent, List<T> allNodes, int currentLeft) {
249
289
/**
250
290
* Rebuild the tree structure starting from the root node.
251
291
*
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.
255
295
*/
256
296
@ Transactional
257
297
protected int rebuildTree (T parent , List <T > allNodes ) {
@@ -261,12 +301,25 @@ protected int rebuildTree(T parent, List<T> allNodes) {
261
301
/**
262
302
* Save all nodes in the tree.
263
303
*
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.
266
306
*/
267
307
protected List <T > saveAllNodes (List <T > nodes ) {
268
308
List <T > savedNodes = repository .saveAll (nodes );
269
309
repository .flush ();
270
310
return savedNodes ;
271
311
}
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
+ }
272
325
}
0 commit comments