This is an automated email from the ASF dual-hosted git repository.

Aias00 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/shenyu.git


The following commit(s) were added to refs/heads/master by this push:
     new 1911e5b2ce [type:fix] Preserve parent metadata and ancestor hierarchy 
on tag upd… (#7800)
1911e5b2ce is described below

commit 1911e5b2ced8b74f88b8c712aaa1bf803e56e3fa
Author: Harsh Raj <[email protected]>
AuthorDate: Fri Oct 23 08:44:17 2026 +0530

    [type:fix] Preserve parent metadata and ancestor hierarchy on tag upd… 
(#7800)
    
    * [type:fix] Preserve parent metadata and ancestor hierarchy on tag update 
(#7772)
    
    * [type:fix] Address review comments on root ext consistency and null tag 
safety (#7772)
    
    ---------
    
    Co-authored-by: Harsh <[email protected]>
---
 .../shenyu/admin/service/impl/TagServiceImpl.java  | 106 +++++++++++++--------
 .../shenyu/admin/service/TagServiceTest.java       |  31 ++++++
 .../admin/service/TagUpdateTransactionTest.java    | 106 ++++++++++++++++++++-
 3 files changed, 202 insertions(+), 41 deletions(-)

diff --git 
a/shenyu-admin/src/main/java/org/apache/shenyu/admin/service/impl/TagServiceImpl.java
 
b/shenyu-admin/src/main/java/org/apache/shenyu/admin/service/impl/TagServiceImpl.java
index 963d0e1bff..4c2b91e5bf 100644
--- 
a/shenyu-admin/src/main/java/org/apache/shenyu/admin/service/impl/TagServiceImpl.java
+++ 
b/shenyu-admin/src/main/java/org/apache/shenyu/admin/service/impl/TagServiceImpl.java
@@ -35,12 +35,13 @@ import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
 
 import java.sql.Timestamp;
-import java.util.List;
+import java.util.HashMap;
 import java.util.HashSet;
-import java.util.Set;
+import java.util.List;
 import java.util.Map;
 import java.util.Objects;
 import java.util.Optional;
+import java.util.Set;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.function.Function;
 import java.util.stream.Collectors;
@@ -95,8 +96,20 @@ public class TagServiceImpl implements TagService {
     public int update(final TagDTO tagDTO) {
         TagDO before = tagMapper.selectByPrimaryKey(tagDTO.getId());
         Assert.notNull(before, "the updated tag is not found");
+        String parentTagId = StringUtils.isNotEmpty(tagDTO.getParentTagId()) ? 
tagDTO.getParentTagId() : before.getParentTagId();
+        if (StringUtils.isEmpty(parentTagId)) {
+            parentTagId = AdminConstants.TAG_ROOT_PARENT_ID;
+        }
         TagDO tagDO = TagDO.buildTagDO(tagDTO);
-        updateSubTags(tagDTO);
+        tagDO.setParentTagId(parentTagId);
+        if (AdminConstants.TAG_ROOT_PARENT_ID.equals(parentTagId)) {
+            tagDO.setExt(buildRootExt(before.getExt()));
+        } else {
+            TagDO parentTagDO = tagMapper.selectByPrimaryKey(parentTagId);
+            Assert.notNull(parentTagDO, "parent tag is not found");
+            tagDO.setExt(buildExtParamByParentTag(parentTagDO, 
before.getExt()));
+        }
+        updateSubTags(tagDO);
         return tagMapper.updateByPrimaryKeySelective(tagDO);
     }
 
@@ -167,25 +180,20 @@ public class TagServiceImpl implements TagService {
     /**
      * update sub tags.
      *
-     * @param tagDTO tagDTO
+     * @param tagDO tagDO
      */
-    private void updateSubTags(final TagDTO tagDTO) {
-        List<TagDO> allData = tagMapper.selectByQuery(new TagQuery());
+    private void updateSubTags(final TagDO tagDO) {
+        List<TagDO> allData = Optional.ofNullable(tagMapper.selectByQuery(new 
TagQuery())).orElseGet(Lists::newArrayList);
         Map<String, TagDO> allDataMap = allData.stream().collect(
-                Collectors.toMap(BaseDO::getId, Function.identity(), (a, b) -> 
b, ConcurrentHashMap::new));
-        TagDO update = TagDO.buildTagDO(tagDTO);
-        allDataMap.put(update.getId(), update);
-        Map<String, List<String>> relationMap = new ConcurrentHashMap<>();
-        allDataMap.keySet().stream().map(allDataMap::get).forEach(tagDO -> {
-            if 
(CollectionUtils.isEmpty(relationMap.get(tagDO.getParentTagId()))) {
-                relationMap.put(tagDO.getParentTagId(), 
Lists.newArrayList(tagDO.getId()));
-            } else {
-                List<String> list = relationMap.get(tagDO.getParentTagId());
-                list.add(tagDO.getId());
-                relationMap.put(tagDO.getParentTagId(), list);
+                Collectors.toMap(BaseDO::getId, Function.identity(), (a, b) -> 
b, HashMap::new));
+        allDataMap.put(tagDO.getId(), tagDO);
+        Map<String, List<String>> relationMap = new HashMap<>();
+        allDataMap.values().forEach(item -> {
+            if (StringUtils.isNotEmpty(item.getParentTagId())) {
+                relationMap.computeIfAbsent(item.getParentTagId(), k -> 
Lists.newArrayList()).add(item.getId());
             }
         });
-        recurseUpdateTag(allDataMap, relationMap, tagDTO.getId(), new 
HashSet<>());
+        recurseUpdateTag(allDataMap, relationMap, tagDO.getId(), new 
HashSet<>());
     }
 
     /**
@@ -204,12 +212,30 @@ public class TagServiceImpl implements TagService {
         List<String> subTagIds = relationMap.get(id);
         subTagIds.forEach(tagId -> {
             TagDO tagDO = allData.get(tagId);
-            tagDO.setExt(buildExtParamByParentTag(allData.get(id)));
+            TagDO parentTagDO = allData.get(id);
+            if (Objects.isNull(tagDO) || Objects.isNull(parentTagDO)) {
+                return;
+            }
+            tagDO.setExt(buildExtParamByParentTag(parentTagDO, 
tagDO.getExt()));
             tagMapper.updateByPrimaryKey(tagDO);
             recurseUpdateTag(allData, relationMap, tagId, visited);
         });
     }
 
+    /**
+     * buildRootExt.
+     *
+     * @param currentExt currentExt
+     * @return ext
+     */
+    private String buildRootExt(final String currentExt) {
+        final TagDO.TagExt ext = StringUtils.isNotEmpty(currentExt)
+                ? 
Optional.ofNullable(GsonUtils.getInstance().fromJson(currentExt, 
TagDO.TagExt.class)).orElse(new TagDO.TagExt())
+                : new TagDO.TagExt();
+        ext.setParent(null);
+        return GsonUtils.getInstance().toJson(ext);
+    }
+
     /**
      * buildExtParam.
      *
@@ -217,26 +243,28 @@ public class TagServiceImpl implements TagService {
      * @return ext
      */
     private String buildExtParamByParentTag(final TagDO parentTagDO) {
-        String ext = "";
-        if (parentTagDO.getId().equals(AdminConstants.TAG_ROOT_PARENT_ID)) {
-            final TagDO.TagExt parent = new TagDO.TagExt();
-            TagDO.TagExt tagExt = new TagDO.TagExt();
-            tagExt.setDesc(parentTagDO.getTagDesc());
-            tagExt.setName(parentTagDO.getTagName());
-            tagExt.setId(parentTagDO.getId());
-            tagExt.setRefreshTime(parent.getRefreshTime());
-            tagExt.setApiDocMd5(parent.getApiDocMd5());
-            parent.setParent(tagExt);
-            ext = GsonUtils.getInstance().toJson(parent);
-        } else {
-            TagDO.TagExt parentTagExt = 
Optional.ofNullable(GsonUtils.getInstance().fromJson(parentTagDO.getExt(), 
TagDO.TagExt.class)).orElse(new TagDO.TagExt());
-            final TagDO.TagExt tagExt = new TagDO.TagExt();
-            parentTagExt.setDesc(parentTagDO.getTagDesc());
-            parentTagExt.setName(parentTagDO.getTagName());
-            parentTagExt.setId(parentTagDO.getId());
-            tagExt.setParent(parentTagExt);
-            ext = GsonUtils.getInstance().toJson(tagExt);
+        return buildExtParamByParentTag(parentTagDO, null);
+    }
+
+    /**
+     * buildExtParam.
+     *
+     * @param parentTagDO parentTagDO
+     * @param currentExt  currentExt
+     * @return ext
+     */
+    private String buildExtParamByParentTag(final TagDO parentTagDO, final 
String currentExt) {
+        if (AdminConstants.TAG_ROOT_PARENT_ID.equals(parentTagDO.getId())) {
+            return buildRootExt(currentExt);
         }
-        return ext;
+        TagDO.TagExt parentTagExt = 
Optional.ofNullable(GsonUtils.getInstance().fromJson(parentTagDO.getExt(), 
TagDO.TagExt.class)).orElse(new TagDO.TagExt());
+        final TagDO.TagExt tagExt = StringUtils.isNotEmpty(currentExt)
+                ? 
Optional.ofNullable(GsonUtils.getInstance().fromJson(currentExt, 
TagDO.TagExt.class)).orElse(new TagDO.TagExt())
+                : new TagDO.TagExt();
+        parentTagExt.setDesc(parentTagDO.getTagDesc());
+        parentTagExt.setName(parentTagDO.getTagName());
+        parentTagExt.setId(parentTagDO.getId());
+        tagExt.setParent(parentTagExt);
+        return GsonUtils.getInstance().toJson(tagExt);
     }
 }
diff --git 
a/shenyu-admin/src/test/java/org/apache/shenyu/admin/service/TagServiceTest.java
 
b/shenyu-admin/src/test/java/org/apache/shenyu/admin/service/TagServiceTest.java
index 79cb1a4ea9..adc1d3a8c5 100644
--- 
a/shenyu-admin/src/test/java/org/apache/shenyu/admin/service/TagServiceTest.java
+++ 
b/shenyu-admin/src/test/java/org/apache/shenyu/admin/service/TagServiceTest.java
@@ -40,6 +40,7 @@ import java.util.List;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
 import static org.junit.jupiter.api.Assertions.assertThrows;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.BDDMockito.given;
@@ -98,6 +99,36 @@ public class TagServiceTest {
         assertEquals(cnt, 1);
     }
 
+    @Test
+    public void testUpdateWithNullParentTagId() {
+        TagDTO tagDTO = new TagDTO();
+        tagDTO.setId("123");
+        tagDTO.setName("name");
+        tagDTO.setParentTagId(null);
+        TagDO before = buildTagDO();
+        before.setParentTagId(null);
+        given(this.tagMapper.selectByPrimaryKey("123")).willReturn(before);
+        given(this.tagMapper.updateByPrimaryKeySelective(any())).willReturn(1);
+        int cnt = tagService.update(tagDTO);
+        assertEquals(cnt, 1);
+        assertNull(tagDTO.getParentTagId());
+    }
+
+    @Test
+    public void testUpdateWithNonExistentTag() {
+        TagDTO tagDTO = buildTagDTO();
+        given(this.tagMapper.selectByPrimaryKey("789")).willReturn(null);
+        assertThrows(ValidFailException.class, () -> 
tagService.update(tagDTO));
+    }
+
+    @Test
+    public void testUpdateWithNonExistentParentTag() {
+        TagDTO tagDTO = buildTagDTO();
+        
given(this.tagMapper.selectByPrimaryKey("789")).willReturn(buildTagDO());
+        given(this.tagMapper.selectByPrimaryKey("456")).willReturn(null);
+        assertThrows(ValidFailException.class, () -> 
tagService.update(tagDTO));
+    }
+
     @Test
     public void testDelete() {
         given(this.tagMapper.deleteByIds(any())).willReturn(1);
diff --git 
a/shenyu-admin/src/test/java/org/apache/shenyu/admin/service/TagUpdateTransactionTest.java
 
b/shenyu-admin/src/test/java/org/apache/shenyu/admin/service/TagUpdateTransactionTest.java
index a974042007..6394a4e7b7 100644
--- 
a/shenyu-admin/src/test/java/org/apache/shenyu/admin/service/TagUpdateTransactionTest.java
+++ 
b/shenyu-admin/src/test/java/org/apache/shenyu/admin/service/TagUpdateTransactionTest.java
@@ -20,8 +20,10 @@ package org.apache.shenyu.admin.service;
 
 import jakarta.annotation.Resource;
 import org.apache.shenyu.admin.AbstractSpringIntegrationTest;
-import org.apache.shenyu.admin.model.dto.TagDTO;
 import org.apache.shenyu.admin.exception.ValidFailException;
+import org.apache.shenyu.admin.model.dto.TagDTO;
+import org.apache.shenyu.admin.model.entity.TagDO;
+import org.apache.shenyu.common.utils.GsonUtils;
 import org.junit.jupiter.api.AfterEach;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
@@ -32,6 +34,8 @@ import org.springframework.jdbc.core.JdbcTemplate;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertNotEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
 import static org.junit.jupiter.api.Assertions.assertThrows;
 
 /**
@@ -49,11 +53,13 @@ public class TagUpdateTransactionTest extends 
AbstractSpringIntegrationTest {
     public void setup() {
         jdbcTemplate.update("INSERT INTO tag (id, tag_name, tag_desc, 
parent_tag_id, ext) VALUES ('update-parent', 'old', '', '0', '{}')");
         jdbcTemplate.update("INSERT INTO tag (id, tag_name, tag_desc, 
parent_tag_id, ext) VALUES ('update-child', 'child', '', 'update-parent', 
'{}')");
+        jdbcTemplate.update("INSERT INTO tag (id, tag_name, tag_desc, 
parent_tag_id, ext) VALUES ('update-grandchild', 'grandchild', '', 
'update-child', '{}')");
+        jdbcTemplate.update("INSERT INTO tag (id, tag_name, tag_desc, 
parent_tag_id, ext) VALUES ('update-parent-2', 'parent2', '', '0', '{}')");
     }
 
     @AfterEach
     public void cleanup() {
-        jdbcTemplate.update("DELETE FROM tag WHERE id IN ('update-child', 
'update-parent')");
+        jdbcTemplate.update("DELETE FROM tag WHERE id IN ('update-grandchild', 
'update-child', 'update-parent', 'update-parent-2')");
     }
 
     @Test
@@ -91,6 +97,102 @@ public class TagUpdateTransactionTest extends 
AbstractSpringIntegrationTest {
         assertNotEquals("{}", childExt());
     }
 
+    @Test
+    public void testUpdateChildPreservesFullAncestryAndUpdatesExt() {
+        TagDTO dto = new TagDTO();
+        dto.setId("update-child");
+        dto.setParentTagId("update-parent");
+        dto.setName("new-child-name");
+        dto.setTagDesc("child-desc");
+        assertEquals(1, tagService.update(dto));
+
+        String childExt = jdbcTemplate.queryForObject("SELECT ext FROM tag 
WHERE id = 'update-child'", String.class);
+        TagDO.TagExt childTagExt = GsonUtils.getInstance().fromJson(childExt, 
TagDO.TagExt.class);
+        assertNotNull(childTagExt);
+        assertNotNull(childTagExt.getParent());
+        assertEquals("update-parent", childTagExt.getParent().getId());
+        assertEquals("old", childTagExt.getParent().getName());
+
+        String grandchildExt = jdbcTemplate.queryForObject("SELECT ext FROM 
tag WHERE id = 'update-grandchild'", String.class);
+        TagDO.TagExt grandchildTagExt = 
GsonUtils.getInstance().fromJson(grandchildExt, TagDO.TagExt.class);
+        assertNotNull(grandchildTagExt);
+        assertNotNull(grandchildTagExt.getParent());
+        assertEquals("update-child", grandchildTagExt.getParent().getId());
+        assertEquals("new-child-name", grandchildTagExt.getParent().getName());
+        assertNotNull(grandchildTagExt.getParent().getParent());
+        assertEquals("update-parent", 
grandchildTagExt.getParent().getParent().getId());
+        assertEquals("old", 
grandchildTagExt.getParent().getParent().getName());
+    }
+
+    @Test
+    public void testReparentChildTagUpdatesExtAndDescendantsAncestry() {
+        TagDTO dto = new TagDTO();
+        dto.setId("update-child");
+        dto.setParentTagId("update-parent-2");
+        dto.setName("child-moved");
+        dto.setTagDesc("child-desc");
+        assertEquals(1, tagService.update(dto));
+
+        String childExt = jdbcTemplate.queryForObject("SELECT ext FROM tag 
WHERE id = 'update-child'", String.class);
+        TagDO.TagExt childTagExt = GsonUtils.getInstance().fromJson(childExt, 
TagDO.TagExt.class);
+        assertNotNull(childTagExt);
+        assertNotNull(childTagExt.getParent());
+        assertEquals("update-parent-2", childTagExt.getParent().getId());
+
+        String grandchildExt = jdbcTemplate.queryForObject("SELECT ext FROM 
tag WHERE id = 'update-grandchild'", String.class);
+        TagDO.TagExt grandchildTagExt = 
GsonUtils.getInstance().fromJson(grandchildExt, TagDO.TagExt.class);
+        assertNotNull(grandchildTagExt);
+        assertNotNull(grandchildTagExt.getParent());
+        assertEquals("update-child", grandchildTagExt.getParent().getId());
+        assertNotNull(grandchildTagExt.getParent().getParent());
+        assertEquals("update-parent-2", 
grandchildTagExt.getParent().getParent().getId());
+    }
+
+    @Test
+    public void testPromoteChildToRootClearsParentInExt() {
+        TagDTO dto = new TagDTO();
+        dto.setId("update-child");
+        dto.setParentTagId("0");
+        dto.setName("child-root");
+        dto.setTagDesc("child-desc");
+        assertEquals(1, tagService.update(dto));
+
+        String childExt = jdbcTemplate.queryForObject("SELECT ext FROM tag 
WHERE id = 'update-child'", String.class);
+        TagDO.TagExt childTagExt = GsonUtils.getInstance().fromJson(childExt, 
TagDO.TagExt.class);
+        assertNotNull(childTagExt);
+        assertNull(childTagExt.getParent());
+
+        String grandchildExt = jdbcTemplate.queryForObject("SELECT ext FROM 
tag WHERE id = 'update-grandchild'", String.class);
+        TagDO.TagExt grandchildTagExt = 
GsonUtils.getInstance().fromJson(grandchildExt, TagDO.TagExt.class);
+        assertNotNull(grandchildTagExt);
+        assertNotNull(grandchildTagExt.getParent());
+        assertEquals("update-child", grandchildTagExt.getParent().getId());
+        assertNull(grandchildTagExt.getParent().getParent());
+    }
+
+    @Test
+    public void testUpdateWithNonExistentParentThrowsValidFailException() {
+        TagDTO dto = new TagDTO();
+        dto.setId("update-child");
+        dto.setParentTagId("non-existent-parent");
+        dto.setName("child");
+        assertThrows(ValidFailException.class, () -> tagService.update(dto));
+    }
+
+    @Test
+    public void testUpdateTagWithoutParentTagIdInDtoPreservesRootExt() {
+        TagDTO dto = new TagDTO();
+        dto.setId("update-parent");
+        dto.setName("new-parent-name");
+        dto.setTagDesc("desc");
+        assertEquals(1, tagService.update(dto));
+
+        String ext = jdbcTemplate.queryForObject("SELECT ext FROM tag WHERE id 
= 'update-parent'", String.class);
+        TagDO.TagExt tagExt = GsonUtils.getInstance().fromJson(ext, 
TagDO.TagExt.class);
+        assertNotNull(tagExt);
+        assertNull(tagExt.getParent());
+    }
+
     private String childExt() {
         return jdbcTemplate.queryForObject("SELECT ext FROM tag WHERE id = 
'update-child'", String.class);
     }

Reply via email to