11package com .mewebstudio .springboot .jpa .nestedset ;
22
3+ import jakarta .persistence .EntityNotFoundException ;
34import jakarta .transaction .Transactional ;
4- import org .apache .commons .lang3 .tuple .Pair ;
55
66import java .util .ArrayList ;
7+ import java .util .Collections ;
78import java .util .Comparator ;
89import java .util .List ;
910import java .util .Optional ;
1011import 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