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)); + } +}
