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

lahirujayathilake pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/airavata-custos.git

commit 95f573c2c67a94bf5c02048a5dc7c7a6a418fff3
Author: lahiruj <[email protected]>
AuthorDate: Mon Oct 13 16:21:36 2025 -0400

    person service impl, new user modify handler, and updates to existing 
handlers
---
 .../amie/handler/DataAccountCreateHandler.java     |  10 +-
 .../amie/handler/RequestAccountCreateHandler.java  |  18 +-
 .../amie/handler/RequestPersonMergeHandler.java    |  28 +--
 ...eHandler.java => RequestUserModifyHandler.java} |  58 ++---
 .../custos/amie/model/ClusterAccountEntity.java    |   3 +
 .../custos/amie/repo/PersonDnsRepository.java      |   6 +
 .../apache/custos/amie/service/PersonService.java  | 242 +++++++++++++++++++++
 7 files changed, 311 insertions(+), 54 deletions(-)

diff --git 
a/amie-decoder/src/main/java/org/apache/custos/amie/handler/DataAccountCreateHandler.java
 
b/amie-decoder/src/main/java/org/apache/custos/amie/handler/DataAccountCreateHandler.java
index 7138e8c78..305629bfd 100644
--- 
a/amie-decoder/src/main/java/org/apache/custos/amie/handler/DataAccountCreateHandler.java
+++ 
b/amie-decoder/src/main/java/org/apache/custos/amie/handler/DataAccountCreateHandler.java
@@ -37,7 +37,7 @@ import java.util.Map;
 @Component
 public class DataAccountCreateHandler implements PacketHandler {
 
-    private static final Logger log = 
LoggerFactory.getLogger(DataAccountCreateHandler.class);
+    private static final Logger LOGGER = 
LoggerFactory.getLogger(DataAccountCreateHandler.class);
 
     private final AmieClient amieClient;
 
@@ -52,7 +52,7 @@ public class DataAccountCreateHandler implements 
PacketHandler {
 
     @Override
     public void handle(JsonNode packetJson, PacketEntity packetEntity) {
-        log.info("Starting 'data_account_create' handler for packet amie_id 
[{}].", packetEntity.getAmieId());
+        LOGGER.info("Starting 'data_account_create' handler for packet amie_id 
[{}].", packetEntity.getAmieId());
 
         JsonNode body = packetJson.path("body");
         String projectId = body.path("ProjectID").asText();
@@ -61,13 +61,13 @@ public class DataAccountCreateHandler implements 
PacketHandler {
 
         Assert.hasText(projectId, "'ProjectID' must not be empty.");
         Assert.hasText(personId, "'PersonID' must not be empty.");
-        log.info("Packet validated. ProjectID: [{}], PersonID: [{}].", 
projectId, personId);
+        LOGGER.info("Packet validated. ProjectID: [{}], PersonID: [{}].", 
projectId, personId);
 
 
         // TODO - perform the business logic
         //  - find the user's record by 'personId' (localID) and update the 
distinguished names (dnList)
         if (dnList.isArray() && !dnList.isEmpty()) {
-            log.info("Received DnList for user [{}]. In a real implementation, 
this would be saved to the user's profile.", personId);
+            LOGGER.info("Received DnList for user [{}]. In a real 
implementation, this would be saved to the user's profile.", personId);
             // TODO userService.updateUserDnList(personId, dnList);
         }
 
@@ -88,6 +88,6 @@ public class DataAccountCreateHandler implements 
PacketHandler {
 
         amieClient.replyToPacket(packetRecId, reply);
 
-        log.info("Successfully sent 'inform_transaction_complete' for 
data_account_create packet_rec_id [{}].", packetRecId);
+        LOGGER.info("Successfully sent 'inform_transaction_complete' for 
data_account_create packet_rec_id [{}].", packetRecId);
     }
 }
diff --git 
a/amie-decoder/src/main/java/org/apache/custos/amie/handler/RequestAccountCreateHandler.java
 
b/amie-decoder/src/main/java/org/apache/custos/amie/handler/RequestAccountCreateHandler.java
index ee8c6dd4e..2e49a8602 100644
--- 
a/amie-decoder/src/main/java/org/apache/custos/amie/handler/RequestAccountCreateHandler.java
+++ 
b/amie-decoder/src/main/java/org/apache/custos/amie/handler/RequestAccountCreateHandler.java
@@ -21,6 +21,7 @@ package org.apache.custos.amie.handler;
 import com.fasterxml.jackson.databind.JsonNode;
 import org.apache.custos.amie.client.AmieClient;
 import org.apache.custos.amie.model.PacketEntity;
+import org.apache.custos.amie.service.PersonService;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.springframework.stereotype.Component;
@@ -43,9 +44,11 @@ public class RequestAccountCreateHandler implements 
PacketHandler {
     private static final Logger LOGGER = 
LoggerFactory.getLogger(RequestAccountCreateHandler.class);
 
     private final AmieClient amieClient;
+    private final PersonService personService;
 
-    public RequestAccountCreateHandler(AmieClient amieClient) {
+    public RequestAccountCreateHandler(AmieClient amieClient, PersonService 
personService) {
         this.amieClient = amieClient;
+        this.personService = personService;
     }
 
     @Override
@@ -62,7 +65,6 @@ public class RequestAccountCreateHandler implements 
PacketHandler {
         String grantNumber = body.path("GrantNumber").asText();
         String userFirstName = body.path("UserFirstName").asText();
         String userLastName = body.path("UserLastName").asText();
-        String userEmail = body.path("UserEmail").asText();
         String userOrgCode = body.path("UserOrgCode").asText();
 
         Assert.hasText(projectId, "'ProjectID' (the local project ID) must not 
be empty.");
@@ -70,11 +72,13 @@ public class RequestAccountCreateHandler implements 
PacketHandler {
         Assert.hasText(userLastName, "'UserLastName' must not be empty.");
         LOGGER.info("Packet validated successfully for user [{} {}] on project 
[{}].", userFirstName, userLastName, projectId);
 
-        // TODO invoke actual cluster's user provisioning service. For the 
time being generating a local user ID and a username
-        String localUserPersonId = UUID.randomUUID().toString();
-        String localUsername = (userFirstName.trim().charAt(0) + 
userLastName.trim().replace(" ", "-")).toLowerCase();
-
-        LOGGER.info("Created local user account with PersonID [{}] and 
username [{}]", localUserPersonId, localUsername);
+        // TODO Replace with external source of truth (e.g., COmanage) lookup 
for PersonID and username
+        String proposedPersonId = UUID.randomUUID().toString();
+        String proposedUsername = (userFirstName.trim().charAt(0) + 
userLastName.trim().replace(" ", "-")).toLowerCase();
+        var provision = personService.createIfAbsentFromPacket(body, 
proposedPersonId, proposedUsername);
+        String localUserPersonId = provision.getLocalPersonId();
+        String localUsername = provision.getUsername();
+        LOGGER.info("Ensured local person [{}] and cluster account username 
[{}] exist.", localUserPersonId, localUsername);
 
         // Build and send the 'notify_account_create' reply
         Map<String, Object> replyBody = new HashMap<>();
diff --git 
a/amie-decoder/src/main/java/org/apache/custos/amie/handler/RequestPersonMergeHandler.java
 
b/amie-decoder/src/main/java/org/apache/custos/amie/handler/RequestPersonMergeHandler.java
index d94419d41..6feda885b 100644
--- 
a/amie-decoder/src/main/java/org/apache/custos/amie/handler/RequestPersonMergeHandler.java
+++ 
b/amie-decoder/src/main/java/org/apache/custos/amie/handler/RequestPersonMergeHandler.java
@@ -21,9 +21,11 @@ package org.apache.custos.amie.handler;
 import com.fasterxml.jackson.databind.JsonNode;
 import org.apache.custos.amie.client.AmieClient;
 import org.apache.custos.amie.model.PacketEntity;
+import org.apache.custos.amie.service.PersonService;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.springframework.stereotype.Component;
+import org.springframework.transaction.annotation.Transactional;
 import org.springframework.util.Assert;
 
 import java.util.HashMap;
@@ -44,9 +46,11 @@ public class RequestPersonMergeHandler implements 
PacketHandler {
     private static final Logger LOGGER = 
LoggerFactory.getLogger(RequestPersonMergeHandler.class);
 
     private final AmieClient amieClient;
+    private final PersonService personService;
 
-    public RequestPersonMergeHandler(AmieClient amieClient) {
+    public RequestPersonMergeHandler(AmieClient amieClient, PersonService 
personService) {
         this.amieClient = amieClient;
+        this.personService = personService;
     }
 
     @Override
@@ -55,25 +59,23 @@ public class RequestPersonMergeHandler implements 
PacketHandler {
     }
 
     @Override
+    @Transactional
     public void handle(JsonNode packetJson, PacketEntity packetEntity) {
         LOGGER.info("Starting 'request_person_merge' handler for packet 
amie_id [{}].", packetEntity.getAmieId());
 
         JsonNode body = packetJson.path("body");
-        String survivingPersonId = body.path("PrimaryPersonID").asText();
-        String retiringPersonId = body.path("PersonID").asText();
+        String survivingPersonLocalId = body.path("KeepPersonID").asText();
+        String survivingPersonGlobalId = body.path("KeepGlobalID").asText();
+        String retiringPersonLocalId = body.path("DeletePersonID").asText();
+        String retiringPersonGlobalId = body.path("DeleteGlobalID").asText();
 
-        Assert.hasText(survivingPersonId, "'PrimaryPersonID' (the surviving 
user) must not be empty.");
-        Assert.hasText(retiringPersonId, "'PersonID' (the retiring user) must 
not be empty.");
-        LOGGER.info("Packet validated. Merging user [{}] into user [{}].", 
retiringPersonId, survivingPersonId);
+        Assert.hasText(survivingPersonLocalId, "'KeepPersonID' (the surviving 
user's local ID) must not be empty.");
+        Assert.hasText(retiringPersonLocalId, "'DeletePersonID' (the retiring 
user's local ID) must not be empty.");
+        LOGGER.info("Packet validated. Merging user with local ID [{}], global 
ID [{}] into user with local ID [{}], global ID [{}].",
+                retiringPersonLocalId, retiringPersonGlobalId, 
survivingPersonLocalId, survivingPersonGlobalId);
 
-        // TODO - perform the business logic
-        //  - find the local user accounts for both the surviving and retiring 
IDs
-        //  - re-associate all project memberships and allocations from the 
retiring user to the surviving user
-        //  - handle the unix system changes
-        //  - lock or disable the retiring user's local login account
-        LOGGER.info("Simulating business logic: Re-associating resources from 
[{}] to [{}] and disabling the retiring account.", retiringPersonId, 
survivingPersonId);
+        personService.mergePersons(survivingPersonLocalId, 
retiringPersonLocalId);
 
-        // Send the 'inform_transaction_complete' reply
         sendSuccessReply(packetEntity.getAmieId());
     }
 
diff --git 
a/amie-decoder/src/main/java/org/apache/custos/amie/handler/DataAccountCreateHandler.java
 
b/amie-decoder/src/main/java/org/apache/custos/amie/handler/RequestUserModifyHandler.java
similarity index 53%
copy from 
amie-decoder/src/main/java/org/apache/custos/amie/handler/DataAccountCreateHandler.java
copy to 
amie-decoder/src/main/java/org/apache/custos/amie/handler/RequestUserModifyHandler.java
index 7138e8c78..62c9db6a6 100644
--- 
a/amie-decoder/src/main/java/org/apache/custos/amie/handler/DataAccountCreateHandler.java
+++ 
b/amie-decoder/src/main/java/org/apache/custos/amie/handler/RequestUserModifyHandler.java
@@ -7,20 +7,21 @@
  * "License"); you may not use this file except in compliance
  * with the License. You may obtain a copy of the License at
  *
- * http://www.apache.org/licenses/LICENSE-2.0
+ *   http://www.apache.org/licenses/LICENSE-2.0
  *
  * Unless required by applicable law or agreed to in writing,
  * software distributed under the License is distributed on an
  * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
  * KIND, either express or implied. See the License for the
- *  specific language governing permissions and limitations
- *  under the License.
+ * specific language governing permissions and limitations
+ * under the License.
  */
 package org.apache.custos.amie.handler;
 
 import com.fasterxml.jackson.databind.JsonNode;
 import org.apache.custos.amie.client.AmieClient;
 import org.apache.custos.amie.model.PacketEntity;
+import org.apache.custos.amie.service.PersonService;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.springframework.stereotype.Component;
@@ -30,48 +31,45 @@ import java.util.HashMap;
 import java.util.Map;
 
 /**
- * Handles the 'data_account_create' packet.
- * <p>
- * This packet is the third step in the account creation transaction.
+ * Handles the 'request_user_modify' AMIE packet (ActionType replace | delete).
  */
 @Component
-public class DataAccountCreateHandler implements PacketHandler {
+public class RequestUserModifyHandler implements PacketHandler {
 
-    private static final Logger log = 
LoggerFactory.getLogger(DataAccountCreateHandler.class);
+    private static final Logger LOGGER = 
LoggerFactory.getLogger(RequestUserModifyHandler.class);
 
     private final AmieClient amieClient;
+    private final PersonService personService;
 
-    public DataAccountCreateHandler(AmieClient amieClient) {
+    public RequestUserModifyHandler(AmieClient amieClient, PersonService 
personService) {
         this.amieClient = amieClient;
+        this.personService = personService;
     }
 
     @Override
     public String supportsType() {
-        return "data_account_create";
+        return "request_user_modify";
     }
 
     @Override
-    public void handle(JsonNode packetJson, PacketEntity packetEntity) {
-        log.info("Starting 'data_account_create' handler for packet amie_id 
[{}].", packetEntity.getAmieId());
+    public void handle(JsonNode packetJson, PacketEntity packetEntity) throws 
Exception {
+        LOGGER.info("Starting 'request_user_modify' handler for packet amie_id 
[{}].", packetEntity.getAmieId());
 
         JsonNode body = packetJson.path("body");
-        String projectId = body.path("ProjectID").asText();
-        String personId = body.path("PersonID").asText();
-        JsonNode dnList = body.path("DnList");
-
-        Assert.hasText(projectId, "'ProjectID' must not be empty.");
-        Assert.hasText(personId, "'PersonID' must not be empty.");
-        log.info("Packet validated. ProjectID: [{}], PersonID: [{}].", 
projectId, personId);
-
-
-        // TODO - perform the business logic
-        //  - find the user's record by 'personId' (localID) and update the 
distinguished names (dnList)
-        if (dnList.isArray() && !dnList.isEmpty()) {
-            log.info("Received DnList for user [{}]. In a real implementation, 
this would be saved to the user's profile.", personId);
-            // TODO userService.updateUserDnList(personId, dnList);
+        String actionType = body.path("ActionType").asText("delete");
+        Assert.hasText(actionType, "'ActionType' must not be empty 
(replace|delete).");
+
+        switch (actionType.toLowerCase()) {
+            case "replace":
+                personService.replaceFromModifyPacket(body);
+                break;
+            case "delete":
+                personService.deleteFromModifyPacket(body);
+                break;
+            default:
+                throw new IllegalArgumentException("Unsupported ActionType: " 
+ actionType);
         }
 
-        // Send the 'inform_transaction_complete' reply to close the 
transaction.
         sendSuccessReply(packetEntity.getAmieId());
     }
 
@@ -81,13 +79,15 @@ public class DataAccountCreateHandler implements 
PacketHandler {
 
         itcBody.put("StatusCode", "Success");
         itcBody.put("DetailCode", 1);
-        itcBody.put("Message", "Transaction completed successfully by 
handler.");
+        itcBody.put("Message", "Transaction completed successfully");
 
         reply.put("type", "inform_transaction_complete");
         reply.put("body", itcBody);
 
         amieClient.replyToPacket(packetRecId, reply);
 
-        log.info("Successfully sent 'inform_transaction_complete' for 
data_account_create packet_rec_id [{}].", packetRecId);
+        LOGGER.info("Successfully sent 'inform_transaction_complete' for 
'request_user_modify' packet_rec_id [{}].", packetRecId);
     }
 }
+
+
diff --git 
a/amie-decoder/src/main/java/org/apache/custos/amie/model/ClusterAccountEntity.java
 
b/amie-decoder/src/main/java/org/apache/custos/amie/model/ClusterAccountEntity.java
index fc45ddf0d..064b33e27 100644
--- 
a/amie-decoder/src/main/java/org/apache/custos/amie/model/ClusterAccountEntity.java
+++ 
b/amie-decoder/src/main/java/org/apache/custos/amie/model/ClusterAccountEntity.java
@@ -21,6 +21,8 @@ package org.apache.custos.amie.model;
 import jakarta.persistence.Column;
 import jakarta.persistence.Entity;
 import jakarta.persistence.FetchType;
+import jakarta.persistence.GeneratedValue;
+import jakarta.persistence.GenerationType;
 import jakarta.persistence.Id;
 import jakarta.persistence.JoinColumn;
 import jakarta.persistence.ManyToOne;
@@ -38,6 +40,7 @@ import java.time.Instant;
 public class ClusterAccountEntity {
 
     @Id
+    @GeneratedValue(strategy = GenerationType.UUID)
     @Column(name = "id")
     private String id;
 
diff --git 
a/amie-decoder/src/main/java/org/apache/custos/amie/repo/PersonDnsRepository.java
 
b/amie-decoder/src/main/java/org/apache/custos/amie/repo/PersonDnsRepository.java
index 5773bc306..21a71194c 100644
--- 
a/amie-decoder/src/main/java/org/apache/custos/amie/repo/PersonDnsRepository.java
+++ 
b/amie-decoder/src/main/java/org/apache/custos/amie/repo/PersonDnsRepository.java
@@ -24,4 +24,10 @@ import org.springframework.stereotype.Repository;
 
 @Repository
 public interface PersonDnsRepository extends JpaRepository<PersonDnsEntity, 
Long> {
+
+    boolean existsByPerson_IdAndDn(String personId, String dn);
+
+    void deleteByPerson_Id(String personId);
+
+    void deleteByPerson_IdAndDnNotIn(String personId, 
java.util.Collection<String> dnsToKeep);
 }
diff --git 
a/amie-decoder/src/main/java/org/apache/custos/amie/service/PersonService.java 
b/amie-decoder/src/main/java/org/apache/custos/amie/service/PersonService.java
new file mode 100644
index 000000000..2b5ecd47f
--- /dev/null
+++ 
b/amie-decoder/src/main/java/org/apache/custos/amie/service/PersonService.java
@@ -0,0 +1,242 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.custos.amie.service;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import org.apache.custos.amie.model.ClusterAccountEntity;
+import org.apache.custos.amie.model.PersonDnsEntity;
+import org.apache.custos.amie.model.PersonEntity;
+import org.apache.custos.amie.repo.ClusterAccountRepository;
+import org.apache.custos.amie.repo.PersonDnsRepository;
+import org.apache.custos.amie.repo.PersonRepository;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.Optional;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+@Service
+public class PersonService {
+
+    private static final Logger LOGGER = 
LoggerFactory.getLogger(PersonService.class);
+
+    private final PersonRepository personRepository;
+    private final PersonDnsRepository personDnsRepository;
+    private final ClusterAccountRepository clusterAccountRepository;
+
+    public PersonService(PersonRepository personRepository, 
PersonDnsRepository personDnsRepository, ClusterAccountRepository 
clusterAccountRepository) {
+        this.personRepository = personRepository;
+        this.personDnsRepository = personDnsRepository;
+        this.clusterAccountRepository = clusterAccountRepository;
+    }
+
+    public static class AccountProvisionResult {
+        private final String localPersonId;
+        private final String username;
+
+        public AccountProvisionResult(String localPersonId, String username) {
+            this.localPersonId = localPersonId;
+            this.username = username;
+        }
+
+        public String getLocalPersonId() {
+            return localPersonId;
+        }
+
+        public String getUsername() {
+            return username;
+        }
+    }
+
+    @Transactional
+    public AccountProvisionResult createIfAbsentFromPacket(JsonNode 
packetBody, String localPersonId, String proposedUsername) {
+        String accessGlobalId = packetBody.path("UserGlobalID").asText(null);
+        if (accessGlobalId == null || accessGlobalId.isBlank()) {
+            LOGGER.warn("Missing required 'UserGlobalID' (ACCESS Global ID) in 
account create packet body");
+            throw new IllegalArgumentException("Missing required 
'UserGlobalID' (ACCESS Global ID) in account create packet body");
+        }
+
+        Optional<PersonEntity> existingByGlobal = 
personRepository.findByAccessGlobalId(accessGlobalId);
+        PersonEntity person;
+        if (existingByGlobal.isPresent()) {
+            person = existingByGlobal.get();
+            LOGGER.info("Person already exists for access_global_id [{}] as 
local id [{}]", accessGlobalId, person.getId());
+
+        } else {
+            person = new PersonEntity();
+            person.setId(localPersonId);
+            person.setAccessGlobalId(accessGlobalId);
+            person.setFirstName(packetBody.path("UserFirstName").asText(""));
+            person.setLastName(packetBody.path("UserLastName").asText(""));
+            person.setEmail(packetBody.path("UserEmail").asText(""));
+            
person.setOrganization(packetBody.path("UserOrganization").asText(null));
+            person.setOrgCode(packetBody.path("UserOrgCode").asText(null));
+            
person.setNsfStatusCode(packetBody.path("NsfStatusCode").asText(null));
+            personRepository.save(person);
+
+            JsonNode dnList = packetBody.path("UserDnList");
+            if (dnList.isArray()) {
+                for (JsonNode dnNode : dnList) {
+                    String dn = dnNode.asText(null);
+                    if (dn == null || dn.isBlank()) continue;
+                    if 
(!personDnsRepository.existsByPerson_IdAndDn(person.getId(), dn)) {
+                        PersonDnsEntity pde = new PersonDnsEntity();
+                        pde.setPerson(person);
+                        pde.setDn(dn);
+                        personDnsRepository.save(pde);
+                    }
+                }
+            }
+        }
+
+        String uniqueUsername = ensureUniqueUsername(proposedUsername);
+        if (clusterAccountRepository.findByUsername(uniqueUsername).isEmpty()) 
{
+            ClusterAccountEntity clusterAccount = new ClusterAccountEntity();
+            clusterAccount.setPerson(person);
+            clusterAccount.setUsername(uniqueUsername);
+            clusterAccount.setActive(true);
+            clusterAccountRepository.save(clusterAccount);
+        } else {
+            LOGGER.error("Username [{}] is already taken by another account", 
uniqueUsername);
+            throw new IllegalStateException("Username " + uniqueUsername + " 
is already taken by another account");
+        }
+
+        return new AccountProvisionResult(person.getId(), uniqueUsername);
+    }
+
+    // TODO - this will not be needed when source of truth (e.g., COmanage) is 
integrated
+    private String ensureUniqueUsername(String base) {
+        String candidate = base;
+        int suffix = 0;
+        while (clusterAccountRepository.findByUsername(candidate).isPresent()) 
{
+            suffix++;
+            candidate = base + suffix;
+        }
+        return candidate;
+    }
+
+    @Transactional
+    public void replaceFromModifyPacket(JsonNode body) {
+        String personId = body.path("PersonID").asText(null);
+        if (personId == null || personId.isBlank()) {
+            LOGGER.warn("Missing required 'PersonID' in request_user_modify 
replace body");
+            throw new IllegalArgumentException("Missing required 'PersonID' in 
request_user_modify replace body");
+        }
+
+        PersonEntity person = 
personRepository.findById(personId).orElseThrow(() -> new 
IllegalArgumentException("Unknown local PersonID: " + personId));
+
+        // Update fields
+        if (body.has("FirstName")) 
person.setFirstName(body.path("FirstName").asText(person.getFirstName()));
+        if (body.has("LastName")) 
person.setLastName(body.path("LastName").asText(person.getLastName()));
+        if (body.has("Email")) 
person.setEmail(body.path("Email").asText(person.getEmail()));
+        person.setOrganization(body.has("Organization") ? 
body.path("Organization").asText(null) : null);
+        person.setOrgCode(body.has("OrgCode") ? 
body.path("OrgCode").asText(null) : null);
+        person.setNsfStatusCode(body.has("NsfStatusCode") ? 
body.path("NsfStatusCode").asText(null) : null);
+        personRepository.save(person);
+
+        Set<String> newDns = new HashSet<>();
+        JsonNode dnList = body.path("DnList");
+        if (dnList != null && dnList.isArray()) {
+            for (JsonNode dnNode : dnList) {
+                String dn = dnNode.asText(null);
+                if (dn != null && !dn.isBlank()) newDns.add(dn);
+            }
+        }
+
+        if (newDns.isEmpty()) {
+            // If no DnList provided in replace, clear all
+            personDnsRepository.deleteByPerson_Id(personId);
+        } else {
+
+            // delete all not in new set
+            personDnsRepository.deleteByPerson_IdAndDnNotIn(personId, new 
ArrayList<>(newDns));
+            // insert missing
+            for (String dn : newDns) {
+                if (!personDnsRepository.existsByPerson_IdAndDn(personId, dn)) 
{
+                    PersonDnsEntity p = new PersonDnsEntity();
+                    p.setPerson(person);
+                    p.setDn(dn);
+                    personDnsRepository.save(p);
+                }
+            }
+        }
+    }
+
+    @Transactional
+    public void deleteFromModifyPacket(JsonNode body) {
+        String personId = body.path("PersonID").asText(null);
+        if (personId == null || personId.isBlank()) {
+            throw new IllegalArgumentException("Missing required 'PersonID' in 
request_user_modify delete body");
+        }
+        // Cascades will remove DNs and cluster accounts
+        personRepository.deleteById(personId);
+    }
+
+    /**
+     * Merges two person records. All cluster accounts and unique DNs from the 
retiring
+     * person are moved to the surviving person. The retiring person record is 
then deleted.
+     *
+     * @param survivingPersonId The local ID of the person to keep.
+     * @param retiringPersonId  The local ID of the person to merge and delete.
+     */
+    @Transactional
+    public void mergePersons(String survivingPersonId, String 
retiringPersonId) {
+        LOGGER.info("Merging person {} into {}", retiringPersonId, 
survivingPersonId);
+
+        PersonEntity survivingPerson = 
personRepository.findById(survivingPersonId)
+                .orElseThrow(() -> new IllegalStateException("Could not find 
surviving person with local ID: " + survivingPersonId));
+
+        PersonEntity retiringPerson = 
personRepository.findById(retiringPersonId)
+                .orElseThrow(() -> new IllegalStateException("Could not find 
retiring person with local ID: " + retiringPersonId));
+
+        // Re-associate all cluster accounts
+        for (ClusterAccountEntity account : 
retiringPerson.getClusterAccounts()) {
+            LOGGER.info("Moving cluster account '{}' from retiring person to 
surviving person", account.getUsername());
+            account.setPerson(survivingPerson);
+            clusterAccountRepository.save(account);
+        }
+
+        // Merge DNs, avoiding duplicates
+        Set<String> survivingDns = survivingPerson.getDnsEntries().stream()
+                .map(PersonDnsEntity::getDn)
+                .collect(Collectors.toSet());
+
+        for (PersonDnsEntity retiringDn : retiringPerson.getDnsEntries()) {
+            if (!survivingDns.contains(retiringDn.getDn())) {
+                LOGGER.info("Moving DN '{}' from retiring person to surviving 
person", retiringDn.getDn());
+                retiringDn.setPerson(survivingPerson);
+                personDnsRepository.save(retiringDn);
+            } else {
+                // Remove the already existing DN
+                personDnsRepository.delete(retiringDn);
+            }
+        }
+
+        // The CASCADE constraint will clean up any remaining associations
+        personRepository.delete(retiringPerson);
+        LOGGER.info("Successfully merged and deleted retiring person record 
{}", retiringPersonId);
+    }
+}
+
+

Reply via email to