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

yasith pushed a commit to branch feat/airavata-service-layer
in repository https://gitbox.apache.org/repos/asf/airavata.git

commit b45863b4d047fd57e0cc5d92a2341b11eb6b855c
Author: yasithdev <[email protected]>
AuthorDate: Thu Mar 26 10:20:50 2026 -0500

    feat: add ProjectService
    
    Extracts createProject, updateProject, deleteProject, getProject,
    getUserProjects, and searchProjects business logic from 
AiravataServerHandler
    into a dedicated ProjectService following the established service layer 
pattern.
---
 .../airavata/service/project/ProjectService.java   | 230 +++++++++++++++++++++
 .../service/project/ProjectServiceTest.java        | 136 ++++++++++++
 2 files changed, 366 insertions(+)

diff --git 
a/airavata-api/src/main/java/org/apache/airavata/service/project/ProjectService.java
 
b/airavata-api/src/main/java/org/apache/airavata/service/project/ProjectService.java
new file mode 100644
index 0000000000..6a428a4eae
--- /dev/null
+++ 
b/airavata-api/src/main/java/org/apache/airavata/service/project/ProjectService.java
@@ -0,0 +1,230 @@
+package org.apache.airavata.service.project;
+
+import org.apache.airavata.common.utils.ServerSettings;
+import org.apache.airavata.model.workspace.Project;
+import org.apache.airavata.model.experiment.ProjectSearchFields;
+import org.apache.airavata.registry.api.service.handler.RegistryServerHandler;
+import org.apache.airavata.service.context.RequestContext;
+import org.apache.airavata.service.exception.ServiceAuthorizationException;
+import org.apache.airavata.service.exception.ServiceException;
+import org.apache.airavata.service.exception.ServiceNotFoundException;
+import org.apache.airavata.sharing.registry.models.Entity;
+import org.apache.airavata.sharing.registry.models.EntitySearchField;
+import org.apache.airavata.sharing.registry.models.SearchCondition;
+import org.apache.airavata.sharing.registry.models.SearchCriteria;
+import 
org.apache.airavata.sharing.registry.server.SharingRegistryServerHandler;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+public class ProjectService {
+
+    private static final Logger logger = 
LoggerFactory.getLogger(ProjectService.class);
+
+    private final RegistryServerHandler registryHandler;
+    private final SharingRegistryServerHandler sharingHandler;
+
+    public ProjectService(RegistryServerHandler registryHandler, 
SharingRegistryServerHandler sharingHandler) {
+        this.registryHandler = registryHandler;
+        this.sharingHandler = sharingHandler;
+    }
+
+    public String createProject(RequestContext ctx, String gatewayId, Project 
project) throws ServiceException {
+        try {
+            String projectId = registryHandler.createProject(gatewayId, 
project);
+
+            if (isSharingEnabled()) {
+                try {
+                    Entity entity = new Entity();
+                    entity.setEntityId(projectId);
+                    final String domainId = project.getGatewayId();
+                    entity.setDomainId(domainId);
+                    entity.setEntityTypeId(domainId + ":" + "PROJECT");
+                    entity.setOwnerId(project.getOwner() + "@" + domainId);
+                    entity.setName(project.getName());
+                    entity.setDescription(project.getDescription());
+                    sharingHandler.createEntity(entity);
+                } catch (Exception ex) {
+                    logger.error("Rolling back project creation Proj ID : {}", 
projectId, ex);
+                    registryHandler.deleteProject(projectId);
+                    throw new ServiceException("Failed to create entry for 
project in Sharing Registry", ex);
+                }
+            }
+
+            logger.debug("Created project with id {} for gateway {}", 
projectId, gatewayId);
+            return projectId;
+        } catch (ServiceException e) {
+            throw e;
+        } catch (Exception e) {
+            throw new ServiceException("Error while creating the project: " + 
e.getMessage(), e);
+        }
+    }
+
+    public void updateProject(RequestContext ctx, String projectId, Project 
updatedProject) throws ServiceException {
+        try {
+            Project existingProject = registryHandler.getProject(projectId);
+            if (existingProject == null) {
+                throw new ServiceNotFoundException("Project " + projectId + " 
does not exist");
+            }
+
+            if (!ctx.getUserId().equals(existingProject.getOwner())
+                    || 
!ctx.getGatewayId().equals(existingProject.getGatewayId())) {
+                if (isSharingEnabled()) {
+                    String qualifiedUserId = ctx.getUserId() + "@" + 
ctx.getGatewayId();
+                    if (!sharingHandler.userHasAccess(
+                            ctx.getGatewayId(), qualifiedUserId, projectId, 
ctx.getGatewayId() + ":WRITE")) {
+                        throw new ServiceAuthorizationException(
+                                "User does not have permission to update this 
resource");
+                    }
+                } else {
+                    throw new ServiceAuthorizationException(
+                            "User does not have permission to update this 
resource");
+                }
+            }
+
+            if (!updatedProject.getOwner().equals(existingProject.getOwner())) 
{
+                throw new ServiceException("Owner of a project cannot be 
changed");
+            }
+            if 
(!updatedProject.getGatewayId().equals(existingProject.getGatewayId())) {
+                throw new ServiceException("Gateway ID of a project cannot be 
changed");
+            }
+
+            registryHandler.updateProject(projectId, updatedProject);
+            logger.debug("Updated project with id {}", projectId);
+        } catch (ServiceException e) {
+            throw e;
+        } catch (Exception e) {
+            throw new ServiceException("Error while updating the project: " + 
e.getMessage(), e);
+        }
+    }
+
+    public boolean deleteProject(RequestContext ctx, String projectId) throws 
ServiceException {
+        try {
+            Project existingProject = registryHandler.getProject(projectId);
+            if (existingProject == null) {
+                throw new ServiceNotFoundException("Project " + projectId + " 
does not exist");
+            }
+
+            if (!ctx.getUserId().equals(existingProject.getOwner())
+                    || 
!ctx.getGatewayId().equals(existingProject.getGatewayId())) {
+                if (isSharingEnabled()) {
+                    String qualifiedUserId = ctx.getUserId() + "@" + 
ctx.getGatewayId();
+                    if (!sharingHandler.userHasAccess(
+                            ctx.getGatewayId(), qualifiedUserId, projectId, 
ctx.getGatewayId() + ":WRITE")) {
+                        throw new ServiceAuthorizationException(
+                                "User does not have permission to delete this 
resource");
+                    }
+                } else {
+                    throw new ServiceAuthorizationException(
+                            "User does not have permission to delete this 
resource");
+                }
+            }
+
+            boolean ret = registryHandler.deleteProject(projectId);
+            logger.debug("Deleted project with id {}", projectId);
+            return ret;
+        } catch (ServiceException e) {
+            throw e;
+        } catch (Exception e) {
+            throw new ServiceException("Error while deleting the project: " + 
e.getMessage(), e);
+        }
+    }
+
+    public Project getProject(RequestContext ctx, String projectId) throws 
ServiceException {
+        try {
+            Project project = registryHandler.getProject(projectId);
+            if (project == null) {
+                throw new ServiceNotFoundException("Project " + projectId + " 
does not exist");
+            }
+
+            if (ctx.getUserId().equals(project.getOwner())
+                    && ctx.getGatewayId().equals(project.getGatewayId())) {
+                return project;
+            }
+
+            if (isSharingEnabled()) {
+                String qualifiedUserId = ctx.getUserId() + "@" + 
ctx.getGatewayId();
+                if (!sharingHandler.userHasAccess(
+                        ctx.getGatewayId(), qualifiedUserId, projectId, 
ctx.getGatewayId() + ":READ")) {
+                    throw new ServiceAuthorizationException(
+                            "User does not have permission to access this 
resource");
+                }
+                return project;
+            }
+
+            return null;
+        } catch (ServiceException e) {
+            throw e;
+        } catch (Exception e) {
+            throw new ServiceException("Error while retrieving the project: " 
+ e.getMessage(), e);
+        }
+    }
+
+    public List<Project> getUserProjects(RequestContext ctx, String gatewayId, 
String userName, int limit, int offset)
+            throws ServiceException {
+        try {
+            if (isSharingEnabled()) {
+                List<String> accessibleProjectIds = new ArrayList<>();
+                List<SearchCriteria> filters = new ArrayList<>();
+                SearchCriteria searchCriteria = new SearchCriteria();
+                
searchCriteria.setSearchField(EntitySearchField.ENTITY_TYPE_ID);
+                searchCriteria.setSearchCondition(SearchCondition.EQUAL);
+                searchCriteria.setValue(gatewayId + ":PROJECT");
+                filters.add(searchCriteria);
+                sharingHandler.searchEntities(
+                        gatewayId, userName + "@" + gatewayId, filters, 0, -1)
+                        .forEach(p -> 
accessibleProjectIds.add(p.getEntityId()));
+
+                if (accessibleProjectIds.isEmpty()) {
+                    return Collections.emptyList();
+                }
+                return registryHandler.searchProjects(
+                        gatewayId, userName, accessibleProjectIds, new 
HashMap<>(), limit, offset);
+            } else {
+                return registryHandler.getUserProjects(gatewayId, userName, 
limit, offset);
+            }
+        } catch (Exception e) {
+            throw new ServiceException("Error while retrieving projects: " + 
e.getMessage(), e);
+        }
+    }
+
+    public List<Project> searchProjects(RequestContext ctx, String gatewayId, 
String userName,
+            Map<ProjectSearchFields, String> filters, int limit, int offset) 
throws ServiceException {
+        try {
+            List<String> accessibleProjIds = new ArrayList<>();
+
+            if (isSharingEnabled()) {
+                List<SearchCriteria> sharingFilters = new ArrayList<>();
+                SearchCriteria searchCriteria = new SearchCriteria();
+                
searchCriteria.setSearchField(EntitySearchField.ENTITY_TYPE_ID);
+                searchCriteria.setSearchCondition(SearchCondition.EQUAL);
+                searchCriteria.setValue(gatewayId + ":PROJECT");
+                sharingFilters.add(searchCriteria);
+                sharingHandler.searchEntities(
+                        gatewayId, userName + "@" + gatewayId, sharingFilters, 
0, Integer.MAX_VALUE)
+                        .forEach(e -> accessibleProjIds.add(e.getEntityId()));
+
+                if (accessibleProjIds.isEmpty()) {
+                    return Collections.emptyList();
+                }
+            }
+
+            return registryHandler.searchProjects(gatewayId, userName, 
accessibleProjIds, filters, limit, offset);
+        } catch (Exception e) {
+            throw new ServiceException("Error while searching projects: " + 
e.getMessage(), e);
+        }
+    }
+
+    private boolean isSharingEnabled() {
+        try {
+            return ServerSettings.isEnableSharing();
+        } catch (Exception e) {
+            return false;
+        }
+    }
+}
diff --git 
a/airavata-api/src/test/java/org/apache/airavata/service/project/ProjectServiceTest.java
 
b/airavata-api/src/test/java/org/apache/airavata/service/project/ProjectServiceTest.java
new file mode 100644
index 0000000000..2d8f54c431
--- /dev/null
+++ 
b/airavata-api/src/test/java/org/apache/airavata/service/project/ProjectServiceTest.java
@@ -0,0 +1,136 @@
+package org.apache.airavata.service.project;
+
+import org.apache.airavata.model.workspace.Project;
+import org.apache.airavata.model.experiment.ProjectSearchFields;
+import org.apache.airavata.registry.api.service.handler.RegistryServerHandler;
+import org.apache.airavata.service.context.RequestContext;
+import org.apache.airavata.service.exception.ServiceAuthorizationException;
+import org.apache.airavata.service.exception.ServiceException;
+import 
org.apache.airavata.sharing.registry.server.SharingRegistryServerHandler;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+import java.util.List;
+import java.util.Map;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.Mockito.*;
+
+@ExtendWith(MockitoExtension.class)
+class ProjectServiceTest {
+
+    @Mock RegistryServerHandler registryHandler;
+    @Mock SharingRegistryServerHandler sharingHandler;
+
+    ProjectService projectService;
+    RequestContext ctx;
+
+    @BeforeEach
+    void setUp() {
+        projectService = new ProjectService(registryHandler, sharingHandler);
+        ctx = new RequestContext("testUser", "testGateway", "token123",
+                Map.of("userName", "testUser", "gatewayId", "testGateway"));
+    }
+
+    @Test
+    void createProject_returnsProjectId() throws Exception {
+        Project project = new Project();
+        project.setName("test-proj");
+        project.setGatewayId("testGateway");
+        project.setOwner("testUser");
+
+        when(registryHandler.createProject("testGateway", 
project)).thenReturn("proj-123");
+
+        String result = projectService.createProject(ctx, "testGateway", 
project);
+
+        assertEquals("proj-123", result);
+        verify(registryHandler).createProject("testGateway", project);
+    }
+
+    @Test
+    void getProject_ownerGetsAccess() throws Exception {
+        Project project = new Project();
+        project.setOwner("testUser");
+        project.setGatewayId("testGateway");
+
+        when(registryHandler.getProject("proj-123")).thenReturn(project);
+
+        Project result = projectService.getProject(ctx, "proj-123");
+
+        assertNotNull(result);
+        assertEquals("testUser", result.getOwner());
+    }
+
+    @Test
+    void getProject_nonOwnerRejectedWhenSharingDisabled() throws Exception {
+        Project project = new Project();
+        project.setOwner("otherUser");
+        project.setGatewayId("testGateway");
+
+        when(registryHandler.getProject("proj-123")).thenReturn(project);
+
+        // sharing disabled (ServerSettings.isEnableSharing() returns false by 
default in tests)
+        Project result = projectService.getProject(ctx, "proj-123");
+
+        assertNull(result);
+        verifyNoInteractions(sharingHandler);
+    }
+
+    @Test
+    void deleteProject_rejectsNonOwnerWithoutWriteAccess() throws Exception {
+        Project project = new Project();
+        project.setOwner("otherUser");
+        project.setGatewayId("testGateway");
+
+        when(registryHandler.getProject("proj-123")).thenReturn(project);
+
+        // sharing disabled — non-owner should be rejected
+        assertThrows(ServiceAuthorizationException.class,
+                () -> projectService.deleteProject(ctx, "proj-123"));
+    }
+
+    @Test
+    void deleteProject_ownerCanDelete() throws Exception {
+        Project project = new Project();
+        project.setOwner("testUser");
+        project.setGatewayId("testGateway");
+
+        when(registryHandler.getProject("proj-123")).thenReturn(project);
+        when(registryHandler.deleteProject("proj-123")).thenReturn(true);
+
+        boolean result = projectService.deleteProject(ctx, "proj-123");
+
+        assertTrue(result);
+        verify(registryHandler).deleteProject("proj-123");
+    }
+
+    @Test
+    void getUserProjects_delegatesToRegistry() throws Exception {
+        List<Project> projects = List.of(new Project(), new Project());
+        when(registryHandler.getUserProjects("testGateway", "testUser", 10, 
0)).thenReturn(projects);
+
+        List<Project> result = projectService.getUserProjects(ctx, 
"testGateway", "testUser", 10, 0);
+
+        assertEquals(2, result.size());
+        verify(registryHandler).getUserProjects("testGateway", "testUser", 10, 
0);
+    }
+
+    @Test
+    void updateProject_rejectsOwnerChange() throws Exception {
+        Project existing = new Project();
+        existing.setOwner("testUser");
+        existing.setGatewayId("testGateway");
+
+        Project updated = new Project();
+        updated.setOwner("newOwner");
+        updated.setGatewayId("testGateway");
+
+        when(registryHandler.getProject("proj-123")).thenReturn(existing);
+
+        assertThrows(ServiceException.class,
+                () -> projectService.updateProject(ctx, "proj-123", updated));
+    }
+}

Reply via email to