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 f466d41349f2bf30fb2fea8707a2ddbd135977f7 Author: yasithdev <[email protected]> AuthorDate: Thu Mar 26 13:12:49 2026 -0500 chore: remove working design docs --- .../plans/2026-03-26-airavata-service-layer.md | 1279 -------------------- .../2026-03-26-airavata-service-layer-design.md | 163 --- 2 files changed, 1442 deletions(-) diff --git a/docs/superpowers/plans/2026-03-26-airavata-service-layer.md b/docs/superpowers/plans/2026-03-26-airavata-service-layer.md deleted file mode 100644 index 1babe51dd5..0000000000 --- a/docs/superpowers/plans/2026-03-26-airavata-service-layer.md +++ /dev/null @@ -1,1279 +0,0 @@ -# airavata-service Layer Implementation Plan - -> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. - -**Goal:** Extract experiment business logic from `AiravataServerHandler` into a service layer package, proving the pattern before migrating remaining domains. - -**Architecture:** New `org.apache.airavata.service.*` package within `airavata-api` containing `ExperimentService` (business logic), `RequestContext` (transport-agnostic identity), service exceptions, and `EventPublisher` (messaging wrapper). The existing handler becomes a thin Thrift transport layer using `ThriftAdapter` to eliminate boilerplate. A separate Maven module would create a circular dependency (model classes, repositories, and handlers all live in `airavata-api`), so the serv [...] - -**Tech Stack:** Java 17, Maven, JUnit 5, Mockito 5, existing Thrift-generated model classes - ---- - -## File Structure - -### New files (all within airavata-api) - -| File | Responsibility | -|------|----------------| -| `.../service/context/RequestContext.java` | Transport-agnostic identity (userId, gatewayId, claims) | -| `.../service/exception/ServiceException.java` | General service error | -| `.../service/exception/ServiceAuthorizationException.java` | Permission denied | -| `.../service/exception/ServiceNotFoundException.java` | Resource not found | -| `.../service/messaging/EventPublisher.java` | Wraps `Publisher` with typed methods for experiment events | -| `.../service/experiment/ExperimentService.java` | Experiment business logic extracted from handler | -| `.../api/server/handler/ThriftAdapter.java` | DRY exception translation + RequestContext construction | -| `...test.../service/context/RequestContextTest.java` | Unit tests for RequestContext | -| `...test.../service/experiment/ExperimentServiceTest.java` | Unit tests for ExperimentService | - -All paths are relative to `airavata-api/src/main/java/org/apache/airavata/` (or `src/test/java/...` for tests). - -### Modified files - -| File | Change | -|------|--------| -| `airavata-api/pom.xml` | Add mockito test dependencies | -| `AiravataServerHandler.java` | Add `ExperimentService` field, rewire experiment methods to one-liner delegates | - ---- - -### Task 1: Add test dependencies to airavata-api POM - -**Files:** -- Modify: `airavata-api/pom.xml` - -- [ ] **Step 1: Add Mockito dependencies** - -In `airavata-api/pom.xml`, add inside the `<dependencies>` block (after the existing JUnit dependency around line 172): - -```xml -<dependency> - <groupId>org.mockito</groupId> - <artifactId>mockito-core</artifactId> - <version>5.11.0</version> - <scope>test</scope> -</dependency> -<dependency> - <groupId>org.mockito</groupId> - <artifactId>mockito-junit-jupiter</artifactId> - <version>5.11.0</version> - <scope>test</scope> -</dependency> -``` - -- [ ] **Step 2: Verify POM resolves** - -```bash -cd /Users/yasith/code/artisan/airavata && mvn validate -pl airavata-api -``` - -Expected: BUILD SUCCESS. - -- [ ] **Step 3: Commit** - -```bash -git add airavata-api/pom.xml -git commit -m "build: add mockito test dependencies to airavata-api" -``` - ---- - -### Task 2: Implement RequestContext - -**Files:** -- Create: `airavata-api/src/main/java/org/apache/airavata/service/context/RequestContext.java` -- Test: `airavata-api/src/test/java/org/apache/airavata/service/context/RequestContextTest.java` - -- [ ] **Step 1: Write the failing test** - -```java -package org.apache.airavata.service.context; - -import org.junit.jupiter.api.Test; - -import java.util.Map; - -import static org.junit.jupiter.api.Assertions.*; - -class RequestContextTest { - - @Test - void constructorSetsFields() { - RequestContext ctx = new RequestContext("testUser", "testGateway", "token123", - Map.of("role", "admin")); - - assertEquals("testUser", ctx.getUserId()); - assertEquals("testGateway", ctx.getGatewayId()); - assertEquals("token123", ctx.getAccessToken()); - assertEquals("admin", ctx.getClaims().get("role")); - } - - @Test - void claimsMapIsUnmodifiable() { - RequestContext ctx = new RequestContext("u", "g", "t", Map.of("k", "v")); - assertThrows(UnsupportedOperationException.class, () -> ctx.getClaims().put("new", "val")); - } -} -``` - -- [ ] **Step 2: Run test to verify it fails** - -```bash -cd /Users/yasith/code/artisan/airavata && mvn test -pl airavata-api -Dtest="org.apache.airavata.service.context.RequestContextTest" -DfailIfNoTests=false -``` - -Expected: compilation error — `RequestContext` class does not exist. - -- [ ] **Step 3: Write the implementation** - -```java -package org.apache.airavata.service.context; - -import java.util.Collections; -import java.util.Map; - -public class RequestContext { - - private final String userId; - private final String gatewayId; - private final String accessToken; - private final Map<String, String> claims; - - public RequestContext(String userId, String gatewayId, String accessToken, Map<String, String> claims) { - this.userId = userId; - this.gatewayId = gatewayId; - this.accessToken = accessToken; - this.claims = Collections.unmodifiableMap(claims); - } - - public String getUserId() { return userId; } - public String getGatewayId() { return gatewayId; } - public String getAccessToken() { return accessToken; } - public Map<String, String> getClaims() { return claims; } -} -``` - -- [ ] **Step 4: Run test to verify it passes** - -```bash -cd /Users/yasith/code/artisan/airavata && mvn test -pl airavata-api -Dtest="org.apache.airavata.service.context.RequestContextTest" -``` - -Expected: 2 tests PASS. - -- [ ] **Step 5: Commit** - -```bash -git add airavata-api/src/main/java/org/apache/airavata/service/context/RequestContext.java \ - airavata-api/src/test/java/org/apache/airavata/service/context/RequestContextTest.java -git commit -m "feat: add RequestContext for transport-agnostic identity" -``` - ---- - -### Task 3: Implement service exceptions - -**Files:** -- Create: `airavata-api/src/main/java/org/apache/airavata/service/exception/ServiceException.java` -- Create: `airavata-api/src/main/java/org/apache/airavata/service/exception/ServiceAuthorizationException.java` -- Create: `airavata-api/src/main/java/org/apache/airavata/service/exception/ServiceNotFoundException.java` - -- [ ] **Step 1: Create ServiceException** - -```java -package org.apache.airavata.service.exception; - -public class ServiceException extends Exception { - - public ServiceException(String message) { - super(message); - } - - public ServiceException(String message, Throwable cause) { - super(message, cause); - } -} -``` - -- [ ] **Step 2: Create ServiceAuthorizationException** - -```java -package org.apache.airavata.service.exception; - -public class ServiceAuthorizationException extends ServiceException { - - public ServiceAuthorizationException(String message) { - super(message); - } - - public ServiceAuthorizationException(String message, Throwable cause) { - super(message, cause); - } -} -``` - -- [ ] **Step 3: Create ServiceNotFoundException** - -```java -package org.apache.airavata.service.exception; - -public class ServiceNotFoundException extends ServiceException { - - public ServiceNotFoundException(String message) { - super(message); - } - - public ServiceNotFoundException(String message, Throwable cause) { - super(message, cause); - } -} -``` - -- [ ] **Step 4: Verify compilation** - -```bash -cd /Users/yasith/code/artisan/airavata && mvn compile -pl airavata-api -``` - -Expected: BUILD SUCCESS. - -- [ ] **Step 5: Commit** - -```bash -git add airavata-api/src/main/java/org/apache/airavata/service/exception/ -git commit -m "feat: add service exception hierarchy" -``` - ---- - -### Task 4: Implement EventPublisher - -**Files:** -- Create: `airavata-api/src/main/java/org/apache/airavata/service/messaging/EventPublisher.java` - -Wraps the existing `Publisher` interface (`org.apache.airavata.messaging.core.Publisher`) with typed methods for experiment events, replacing the `MessageContext` construction scattered across `AiravataServerHandler` (lines 6469-6506). - -- [ ] **Step 1: Write EventPublisher** - -```java -package org.apache.airavata.service.messaging; - -import org.apache.airavata.common.exception.AiravataException; -import org.apache.airavata.common.utils.AiravataUtils; -import org.apache.airavata.messaging.core.MessageContext; -import org.apache.airavata.messaging.core.Publisher; -import org.apache.airavata.model.messaging.event.ExperimentStatusChangeEvent; -import org.apache.airavata.model.messaging.event.ExperimentSubmitEvent; -import org.apache.airavata.model.messaging.event.ExperimentIntermediateOutputsEvent; -import org.apache.airavata.model.messaging.event.MessageType; -import org.apache.airavata.model.status.ExperimentState; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.util.List; -import java.util.UUID; - -public class EventPublisher { - - private static final Logger logger = LoggerFactory.getLogger(EventPublisher.class); - - private final Publisher statusPublisher; - private final Publisher experimentPublisher; - - public EventPublisher(Publisher statusPublisher, Publisher experimentPublisher) { - this.statusPublisher = statusPublisher; - this.experimentPublisher = experimentPublisher; - } - - public void publishExperimentStatus(String experimentId, String gatewayId, ExperimentState state) { - if (statusPublisher == null) return; - try { - ExperimentStatusChangeEvent event = new ExperimentStatusChangeEvent(state, experimentId, gatewayId); - String messageId = AiravataUtils.getId("EXPERIMENT"); - MessageContext messageContext = new MessageContext(event, MessageType.EXPERIMENT, messageId, gatewayId); - messageContext.setUpdatedTime(AiravataUtils.getCurrentTimestamp()); - statusPublisher.publish(messageContext); - } catch (AiravataException e) { - logger.error("Failed to publish experiment status event for {}", experimentId, e); - } - } - - public void publishExperimentLaunch(String experimentId, String gatewayId) { - if (experimentPublisher == null) return; - try { - ExperimentSubmitEvent event = new ExperimentSubmitEvent(experimentId, gatewayId); - MessageContext messageContext = new MessageContext( - event, MessageType.EXPERIMENT, "LAUNCH.EXP-" + UUID.randomUUID(), gatewayId); - messageContext.setUpdatedTime(AiravataUtils.getCurrentTimestamp()); - experimentPublisher.publish(messageContext); - } catch (AiravataException e) { - logger.error("Failed to publish experiment launch event for {}", experimentId, e); - } - } - - public void publishExperimentCancel(String experimentId, String gatewayId) { - if (experimentPublisher == null) return; - try { - ExperimentSubmitEvent event = new ExperimentSubmitEvent(experimentId, gatewayId); - MessageContext messageContext = new MessageContext( - event, MessageType.EXPERIMENT_CANCEL, "CANCEL.EXP-" + UUID.randomUUID(), gatewayId); - messageContext.setUpdatedTime(AiravataUtils.getCurrentTimestamp()); - experimentPublisher.publish(messageContext); - } catch (AiravataException e) { - logger.error("Failed to publish experiment cancel event for {}", experimentId, e); - } - } - - public void publishIntermediateOutputs(String experimentId, String gatewayId, List<String> outputNames) { - if (experimentPublisher == null) return; - try { - ExperimentIntermediateOutputsEvent event = - new ExperimentIntermediateOutputsEvent(experimentId, gatewayId, outputNames); - MessageContext messageContext = new MessageContext( - event, MessageType.INTERMEDIATE_OUTPUTS, - "INTERMEDIATE_OUTPUTS.EXP-" + UUID.randomUUID(), gatewayId); - messageContext.setUpdatedTime(AiravataUtils.getCurrentTimestamp()); - experimentPublisher.publish(messageContext); - } catch (AiravataException e) { - logger.error("Failed to publish intermediate outputs event for {}", experimentId, e); - } - } -} -``` - -- [ ] **Step 2: Verify compilation** - -```bash -cd /Users/yasith/code/artisan/airavata && mvn compile -pl airavata-api -``` - -Expected: BUILD SUCCESS. - -- [ ] **Step 3: Commit** - -```bash -git add airavata-api/src/main/java/org/apache/airavata/service/messaging/EventPublisher.java -git commit -m "feat: add EventPublisher wrapping messaging infrastructure" -``` - ---- - -### Task 5: Implement ExperimentService — createExperiment and getExperiment - -**Files:** -- Create: `airavata-api/src/main/java/org/apache/airavata/service/experiment/ExperimentService.java` -- Create: `airavata-api/src/test/java/org/apache/airavata/service/experiment/ExperimentServiceTest.java` - -Extracts `createExperiment` from `AiravataServerHandler` lines 1417-1472 and `getExperiment` from lines 1554-1594. - -- [ ] **Step 1: Write the failing test** - -```java -package org.apache.airavata.service.experiment; - -import org.apache.airavata.model.experiment.ExperimentModel; -import org.apache.airavata.registry.api.service.handler.RegistryServerHandler; -import org.apache.airavata.service.context.RequestContext; -import org.apache.airavata.service.messaging.EventPublisher; -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.Map; - -import static org.junit.jupiter.api.Assertions.*; -import static org.mockito.Mockito.*; - -@ExtendWith(MockitoExtension.class) -class ExperimentServiceTest { - - @Mock RegistryServerHandler registryHandler; - @Mock SharingRegistryServerHandler sharingHandler; - @Mock EventPublisher eventPublisher; - - ExperimentService experimentService; - RequestContext ctx; - - @BeforeEach - void setUp() { - experimentService = new ExperimentService(registryHandler, sharingHandler, eventPublisher); - ctx = new RequestContext("testUser", "testGateway", "token123", - Map.of("userName", "testUser", "gatewayId", "testGateway")); - } - - @Test - void createExperiment_returnsExperimentId() throws Exception { - ExperimentModel experiment = new ExperimentModel(); - experiment.setExperimentName("test-exp"); - experiment.setGatewayId("testGateway"); - experiment.setUserName("testUser"); - experiment.setProjectId("proj-1"); - - when(registryHandler.createExperiment("testGateway", experiment)).thenReturn("exp-123"); - - String result = experimentService.createExperiment(ctx, experiment); - - assertEquals("exp-123", result); - verify(registryHandler).createExperiment("testGateway", experiment); - } - - @Test - void getExperiment_ownerGetsAccess() throws Exception { - ExperimentModel experiment = new ExperimentModel(); - experiment.setUserName("testUser"); - experiment.setGatewayId("testGateway"); - - when(registryHandler.getExperiment("exp-123")).thenReturn(experiment); - - ExperimentModel result = experimentService.getExperiment(ctx, "exp-123"); - - assertNotNull(result); - assertEquals("testUser", result.getUserName()); - } -} -``` - -- [ ] **Step 2: Run test to verify it fails** - -```bash -cd /Users/yasith/code/artisan/airavata && mvn test -pl airavata-api -Dtest="org.apache.airavata.service.experiment.ExperimentServiceTest" -DfailIfNoTests=false -``` - -Expected: compilation error — `ExperimentService` does not exist. - -- [ ] **Step 3: Write ExperimentService with createExperiment and getExperiment** - -```java -package org.apache.airavata.service.experiment; - -import org.apache.airavata.common.utils.ServerSettings; -import org.apache.airavata.model.experiment.ExperimentModel; -import org.apache.airavata.model.experiment.ExperimentSummaryModel; -import org.apache.airavata.model.experiment.ExperimentSearchFields; -import org.apache.airavata.model.application.io.OutputDataObjectType; -import org.apache.airavata.model.status.ExperimentState; -import org.apache.airavata.model.status.ExperimentStatus; -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.service.messaging.EventPublisher; -import org.apache.airavata.sharing.registry.models.Entity; -import org.apache.airavata.sharing.registry.models.SearchCriteria; -import org.apache.airavata.sharing.registry.models.EntitySearchField; -import org.apache.airavata.sharing.registry.models.SearchCondition; -import org.apache.airavata.sharing.registry.server.SharingRegistryServerHandler; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -public class ExperimentService { - - private static final Logger logger = LoggerFactory.getLogger(ExperimentService.class); - - private final RegistryServerHandler registryHandler; - private final SharingRegistryServerHandler sharingHandler; - private final EventPublisher eventPublisher; - - public ExperimentService( - RegistryServerHandler registryHandler, - SharingRegistryServerHandler sharingHandler, - EventPublisher eventPublisher) { - this.registryHandler = registryHandler; - this.sharingHandler = sharingHandler; - this.eventPublisher = eventPublisher; - } - - public String createExperiment(RequestContext ctx, ExperimentModel experiment) throws ServiceException { - try { - String experimentId = registryHandler.createExperiment(ctx.getGatewayId(), experiment); - - if (ServerSettings.isEnableSharing()) { - try { - Entity entity = new Entity(); - entity.setEntityId(experimentId); - String domainId = experiment.getGatewayId(); - entity.setDomainId(domainId); - entity.setEntityTypeId(domainId + ":" + "EXPERIMENT"); - entity.setOwnerId(experiment.getUserName() + "@" + domainId); - entity.setName(experiment.getExperimentName()); - entity.setDescription(experiment.getDescription()); - entity.setParentEntityId(experiment.getProjectId()); - sharingHandler.createEntity(entity); - } catch (Exception ex) { - logger.error("Rolling back experiment creation Exp ID : {}", experimentId, ex); - registryHandler.deleteExperiment(experimentId); - throw new ServiceException("Failed to create sharing registry record", ex); - } - } - - eventPublisher.publishExperimentStatus(experimentId, ctx.getGatewayId(), ExperimentState.CREATED); - logger.info("Created new experiment with name {} and id {}", - experiment.getExperimentName(), experimentId); - return experimentId; - } catch (ServiceException e) { - throw e; - } catch (Exception e) { - throw new ServiceException("Error while creating the experiment: " + e.getMessage(), e); - } - } - - public ExperimentModel getExperiment(RequestContext ctx, String experimentId) throws ServiceException { - try { - ExperimentModel experiment = registryHandler.getExperiment(experimentId); - if (experiment == null) { - throw new ServiceNotFoundException("Experiment " + experimentId + " does not exist"); - } - - // Owner always has access - if (ctx.getUserId().equals(experiment.getUserName()) - && ctx.getGatewayId().equals(experiment.getGatewayId())) { - return experiment; - } - - // Check sharing permissions - if (ServerSettings.isEnableSharing()) { - String qualifiedUserId = ctx.getUserId() + "@" + ctx.getGatewayId(); - if (!sharingHandler.userHasAccess( - ctx.getGatewayId(), qualifiedUserId, experimentId, ctx.getGatewayId() + ":READ")) { - throw new ServiceAuthorizationException( - "User does not have permission to access this resource"); - } - return experiment; - } - - return null; - } catch (ServiceException e) { - throw e; - } catch (Exception e) { - throw new ServiceException("Error while getting the experiment: " + e.getMessage(), e); - } - } -} -``` - -- [ ] **Step 4: Run tests to verify they pass** - -```bash -cd /Users/yasith/code/artisan/airavata && mvn test -pl airavata-api -Dtest="org.apache.airavata.service.experiment.ExperimentServiceTest" -``` - -Expected: 2 tests PASS. - -- [ ] **Step 5: Commit** - -```bash -git add airavata-api/src/main/java/org/apache/airavata/service/experiment/ExperimentService.java \ - airavata-api/src/test/java/org/apache/airavata/service/experiment/ExperimentServiceTest.java -git commit -m "feat: add ExperimentService with createExperiment and getExperiment" -``` - ---- - -### Task 6: Add remaining ExperimentService methods - -**Files:** -- Modify: `airavata-api/src/main/java/org/apache/airavata/service/experiment/ExperimentService.java` -- Modify: `airavata-api/src/test/java/org/apache/airavata/service/experiment/ExperimentServiceTest.java` - -Adds `deleteExperiment`, `getExperimentByAdmin`, `searchExperiments`, `getExperimentStatus`, `getExperimentOutputs`, `terminateExperiment`, `cloneExperiment`. Business logic extracted from `AiravataServerHandler` lines 1487-1528, 1598-1617, 1134-1235, 1845-1854, 1858-1870, 2413-2453, 2250-2370. - -- [ ] **Step 1: Write failing tests for new methods** - -Add to `ExperimentServiceTest.java`: - -```java -import org.apache.airavata.model.status.ExperimentState; -import org.apache.airavata.model.status.ExperimentStatus; -import org.apache.airavata.model.application.io.OutputDataObjectType; -import org.apache.airavata.service.exception.ServiceAuthorizationException; -import org.apache.airavata.service.exception.ServiceException; - -import java.util.List; - -// Add these test methods inside the class: - -@Test -void deleteExperiment_onlyDeletesCreatedExperiments() throws Exception { - ExperimentModel experiment = new ExperimentModel(); - experiment.setUserName("testUser"); - experiment.setGatewayId("testGateway"); - ExperimentStatus status = new ExperimentStatus(); - status.setState(ExperimentState.CREATED); - experiment.addToExperimentStatus(status); - - when(registryHandler.getExperiment("exp-123")).thenReturn(experiment); - when(registryHandler.deleteExperiment("exp-123")).thenReturn(true); - - boolean result = experimentService.deleteExperiment(ctx, "exp-123"); - - assertTrue(result); - verify(registryHandler).deleteExperiment("exp-123"); -} - -@Test -void deleteExperiment_rejectsNonCreatedExperiment() throws Exception { - ExperimentModel experiment = new ExperimentModel(); - experiment.setUserName("testUser"); - experiment.setGatewayId("testGateway"); - ExperimentStatus status = new ExperimentStatus(); - status.setState(ExperimentState.EXECUTING); - experiment.addToExperimentStatus(status); - - when(registryHandler.getExperiment("exp-123")).thenReturn(experiment); - - assertThrows(ServiceException.class, - () -> experimentService.deleteExperiment(ctx, "exp-123")); -} - -@Test -void getExperimentByAdmin_allowsSameGateway() throws Exception { - ExperimentModel experiment = new ExperimentModel(); - experiment.setUserName("otherUser"); - experiment.setGatewayId("testGateway"); - - when(registryHandler.getExperiment("exp-123")).thenReturn(experiment); - - ExperimentModel result = experimentService.getExperimentByAdmin(ctx, "exp-123"); - - assertNotNull(result); -} - -@Test -void getExperimentByAdmin_rejectsDifferentGateway() throws Exception { - ExperimentModel experiment = new ExperimentModel(); - experiment.setUserName("otherUser"); - experiment.setGatewayId("otherGateway"); - - when(registryHandler.getExperiment("exp-123")).thenReturn(experiment); - - assertThrows(ServiceAuthorizationException.class, - () -> experimentService.getExperimentByAdmin(ctx, "exp-123")); -} - -@Test -void getExperimentStatus_delegatesToRegistry() throws Exception { - ExperimentStatus status = new ExperimentStatus(); - status.setState(ExperimentState.COMPLETED); - when(registryHandler.getExperimentStatus("exp-123")).thenReturn(status); - - ExperimentStatus result = experimentService.getExperimentStatus(ctx, "exp-123"); - - assertEquals(ExperimentState.COMPLETED, result.getState()); -} - -@Test -void getExperimentOutputs_delegatesToRegistry() throws Exception { - List<OutputDataObjectType> outputs = List.of(new OutputDataObjectType()); - when(registryHandler.getExperimentOutputs("exp-123")).thenReturn(outputs); - - List<OutputDataObjectType> result = experimentService.getExperimentOutputs(ctx, "exp-123"); - - assertEquals(1, result.size()); -} -``` - -- [ ] **Step 2: Run tests to verify they fail** - -```bash -cd /Users/yasith/code/artisan/airavata && mvn test -pl airavata-api -Dtest="org.apache.airavata.service.experiment.ExperimentServiceTest" -DfailIfNoTests=false -``` - -Expected: compilation errors — methods don't exist on `ExperimentService`. - -- [ ] **Step 3: Add the methods to ExperimentService** - -Add these methods to `ExperimentService.java`: - -```java -public boolean deleteExperiment(RequestContext ctx, String experimentId) throws ServiceException { - try { - ExperimentModel experiment = registryHandler.getExperiment(experimentId); - - if (!ctx.getUserId().equals(experiment.getUserName()) - || !ctx.getGatewayId().equals(experiment.getGatewayId())) { - if (ServerSettings.isEnableSharing()) { - String qualifiedUserId = ctx.getUserId() + "@" + ctx.getGatewayId(); - if (!sharingHandler.userHasAccess( - ctx.getGatewayId(), qualifiedUserId, experimentId, - ctx.getGatewayId() + ":WRITE")) { - throw new ServiceAuthorizationException( - "User does not have permission to delete this resource"); - } - } - } - - if (experiment.getExperimentStatus().get(0).getState() != ExperimentState.CREATED) { - throw new ServiceException( - "Experiment is not in CREATED state. Cannot be deleted. ID: " + experimentId); - } - - return registryHandler.deleteExperiment(experimentId); - } catch (ServiceException e) { - throw e; - } catch (Exception e) { - throw new ServiceException("Error while deleting the experiment: " + e.getMessage(), e); - } -} - -public ExperimentModel getExperimentByAdmin(RequestContext ctx, String experimentId) - throws ServiceException { - try { - ExperimentModel experiment = registryHandler.getExperiment(experimentId); - if (ctx.getGatewayId().equals(experiment.getGatewayId())) { - return experiment; - } - throw new ServiceAuthorizationException( - "User does not have permission to access this resource"); - } catch (ServiceException e) { - throw e; - } catch (Exception e) { - throw new ServiceException("Error while getting the experiment: " + e.getMessage(), e); - } -} - -public List<ExperimentSummaryModel> searchExperiments( - RequestContext ctx, String gatewayId, String userName, - Map<ExperimentSearchFields, String> filters, int limit, int offset) - throws ServiceException { - try { - List<String> accessibleExpIds = new ArrayList<>(); - Map<ExperimentSearchFields, String> filtersCopy = new HashMap<>(filters); - List<SearchCriteria> sharingFilters = new ArrayList<>(); - - SearchCriteria entityTypeCriteria = new SearchCriteria(); - entityTypeCriteria.setSearchField(EntitySearchField.ENTITY_TYPE_ID); - entityTypeCriteria.setSearchCondition(SearchCondition.EQUAL); - entityTypeCriteria.setValue(gatewayId + ":EXPERIMENT"); - sharingFilters.add(entityTypeCriteria); - - if (filtersCopy.containsKey(ExperimentSearchFields.FROM_DATE)) { - String fromTime = filtersCopy.remove(ExperimentSearchFields.FROM_DATE); - SearchCriteria c = new SearchCriteria(); - c.setSearchField(EntitySearchField.CREATED_TIME); - c.setSearchCondition(SearchCondition.GTE); - c.setValue(fromTime); - sharingFilters.add(c); - } - if (filtersCopy.containsKey(ExperimentSearchFields.TO_DATE)) { - String toTime = filtersCopy.remove(ExperimentSearchFields.TO_DATE); - SearchCriteria c = new SearchCriteria(); - c.setSearchField(EntitySearchField.CREATED_TIME); - c.setSearchCondition(SearchCondition.LTE); - c.setValue(toTime); - sharingFilters.add(c); - } - if (filtersCopy.containsKey(ExperimentSearchFields.PROJECT_ID)) { - String projectId = filtersCopy.remove(ExperimentSearchFields.PROJECT_ID); - SearchCriteria c = new SearchCriteria(); - c.setSearchField(EntitySearchField.PARRENT_ENTITY_ID); - c.setSearchCondition(SearchCondition.EQUAL); - c.setValue(projectId); - sharingFilters.add(c); - } - if (filtersCopy.containsKey(ExperimentSearchFields.USER_NAME)) { - String username = filtersCopy.remove(ExperimentSearchFields.USER_NAME); - SearchCriteria c = new SearchCriteria(); - c.setSearchField(EntitySearchField.OWNER_ID); - c.setSearchCondition(SearchCondition.EQUAL); - c.setValue(username + "@" + gatewayId); - sharingFilters.add(c); - } - if (filtersCopy.containsKey(ExperimentSearchFields.EXPERIMENT_NAME)) { - String name = filtersCopy.remove(ExperimentSearchFields.EXPERIMENT_NAME); - SearchCriteria c = new SearchCriteria(); - c.setSearchField(EntitySearchField.NAME); - c.setSearchCondition(SearchCondition.LIKE); - c.setValue(name); - sharingFilters.add(c); - } - if (filtersCopy.containsKey(ExperimentSearchFields.EXPERIMENT_DESC)) { - String desc = filtersCopy.remove(ExperimentSearchFields.EXPERIMENT_DESC); - SearchCriteria c = new SearchCriteria(); - c.setSearchField(EntitySearchField.DESCRIPTION); - c.setSearchCondition(SearchCondition.LIKE); - c.setValue(desc); - sharingFilters.add(c); - } - - int searchOffset = 0; - int searchLimit = Integer.MAX_VALUE; - boolean filteredInSharing = filtersCopy.isEmpty(); - if (filteredInSharing) { - searchOffset = offset; - searchLimit = limit; - } - - sharingHandler.searchEntities( - gatewayId, userName + "@" + gatewayId, - sharingFilters, searchOffset, searchLimit) - .forEach(e -> accessibleExpIds.add(e.getEntityId())); - - int finalOffset = filteredInSharing ? 0 : offset; - return registryHandler.searchExperiments( - gatewayId, userName, accessibleExpIds, filtersCopy, limit, finalOffset); - } catch (Exception e) { - throw new ServiceException("Error while searching experiments: " + e.getMessage(), e); - } -} - -public ExperimentStatus getExperimentStatus(RequestContext ctx, String experimentId) - throws ServiceException { - try { - return registryHandler.getExperimentStatus(experimentId); - } catch (Exception e) { - throw new ServiceException( - "Error while getting experiment status: " + e.getMessage(), e); - } -} - -public List<OutputDataObjectType> getExperimentOutputs(RequestContext ctx, String experimentId) - throws ServiceException { - try { - return registryHandler.getExperimentOutputs(experimentId); - } catch (Exception e) { - throw new ServiceException( - "Error while retrieving experiment outputs: " + e.getMessage(), e); - } -} - -public void terminateExperiment(RequestContext ctx, String experimentId) - throws ServiceException { - try { - ExperimentModel experiment = registryHandler.getExperiment(experimentId); - if (experiment == null) { - throw new ServiceNotFoundException( - "Experiment " + experimentId + " does not exist"); - } - ExperimentStatus status = registryHandler.getExperimentStatus(experimentId); - switch (status.getState()) { - case COMPLETED: - case CANCELED: - case FAILED: - case CANCELING: - logger.warn("Can't terminate already {} experiment", - status.getState().name()); - return; - case CREATED: - logger.warn("Experiment termination is only allowed for launched experiments."); - return; - default: - eventPublisher.publishExperimentCancel(experimentId, ctx.getGatewayId()); - logger.debug("Cancelled experiment {}", experimentId); - } - } catch (ServiceException e) { - throw e; - } catch (Exception e) { - throw new ServiceException( - "Error while cancelling the experiment: " + e.getMessage(), e); - } -} - -public String cloneExperiment(RequestContext ctx, String existingExperimentId, - String newExperimentName, String newExperimentProjectId, - boolean adminMode) throws ServiceException { - try { - ExperimentModel existingExperiment; - if (adminMode) { - existingExperiment = getExperimentByAdmin(ctx, existingExperimentId); - } else { - existingExperiment = getExperiment(ctx, existingExperimentId); - } - - if (existingExperiment == null) { - throw new ServiceNotFoundException( - "Experiment " + existingExperimentId + " does not exist"); - } - - if (newExperimentProjectId != null) { - existingExperiment.setProjectId(newExperimentProjectId); - } - - // Verify write access to target project - String qualifiedUserId = ctx.getUserId() + "@" + ctx.getGatewayId(); - if (!sharingHandler.userHasAccess( - ctx.getGatewayId(), qualifiedUserId, - existingExperiment.getProjectId(), ctx.getGatewayId() + ":WRITE")) { - throw new ServiceAuthorizationException( - "User does not have permission to clone an experiment in this project"); - } - - existingExperiment.setCreationTime(System.currentTimeMillis()); - if (existingExperiment.getExecutionId() != null) { - List<OutputDataObjectType> appOutputs = - registryHandler.getApplicationOutputs(existingExperiment.getExecutionId()); - existingExperiment.setExperimentOutputs(appOutputs); - } - if (newExperimentName != null && !newExperimentName.isEmpty()) { - existingExperiment.setExperimentName(newExperimentName); - } - existingExperiment.unsetErrors(); - existingExperiment.unsetProcesses(); - existingExperiment.unsetExperimentStatus(); - - return createExperiment(ctx, existingExperiment); - } catch (ServiceException e) { - throw e; - } catch (Exception e) { - throw new ServiceException( - "Error while cloning experiment: " + e.getMessage(), e); - } -} -``` - -- [ ] **Step 4: Run tests to verify they pass** - -```bash -cd /Users/yasith/code/artisan/airavata && mvn test -pl airavata-api -Dtest="org.apache.airavata.service.experiment.ExperimentServiceTest" -``` - -Expected: 8 tests PASS (2 from Task 5 + 6 new). - -- [ ] **Step 5: Commit** - -```bash -git add airavata-api/src/main/java/org/apache/airavata/service/experiment/ExperimentService.java \ - airavata-api/src/test/java/org/apache/airavata/service/experiment/ExperimentServiceTest.java -git commit -m "feat: add remaining ExperimentService methods" -``` - ---- - -### Task 7: Implement ThriftAdapter - -**Files:** -- Create: `airavata-api/src/main/java/org/apache/airavata/api/server/handler/ThriftAdapter.java` - -- [ ] **Step 1: Write ThriftAdapter** - -```java -package org.apache.airavata.api.server.handler; - -import org.apache.airavata.common.utils.Constants; -import org.apache.airavata.model.error.AiravataErrorType; -import org.apache.airavata.model.error.AiravataSystemException; -import org.apache.airavata.model.error.AuthorizationException; -import org.apache.airavata.model.error.ExperimentNotFoundException; -import org.apache.airavata.model.security.AuthzToken; -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 java.util.Map; - -public class ThriftAdapter { - - @FunctionalInterface - public interface ServiceCall<T> { - T apply(RequestContext ctx) throws Exception; - } - - @FunctionalInterface - public interface ServiceVoidCall { - void apply(RequestContext ctx) throws Exception; - } - - public static <T> T execute(AuthzToken authzToken, String gatewayId, ServiceCall<T> call) - throws AiravataSystemException, AuthorizationException { - try { - RequestContext ctx = toRequestContext(authzToken, gatewayId); - return call.apply(ctx); - } catch (ServiceAuthorizationException e) { - throw new AuthorizationException(e.getMessage()); - } catch (ServiceNotFoundException e) { - throw new ExperimentNotFoundException(e.getMessage()); - } catch (ServiceException e) { - AiravataSystemException ase = new AiravataSystemException(); - ase.setAiravataErrorType(AiravataErrorType.INTERNAL_ERROR); - ase.setMessage(e.getMessage()); - throw ase; - } catch (AuthorizationException | AiravataSystemException e) { - throw e; - } catch (Exception e) { - AiravataSystemException ase = new AiravataSystemException(); - ase.setAiravataErrorType(AiravataErrorType.INTERNAL_ERROR); - ase.setMessage(e.getMessage()); - throw ase; - } - } - - public static void executeVoid(AuthzToken authzToken, String gatewayId, ServiceVoidCall call) - throws AiravataSystemException, AuthorizationException { - execute(authzToken, gatewayId, ctx -> { - call.apply(ctx); - return null; - }); - } - - private static RequestContext toRequestContext(AuthzToken authzToken, String gatewayId) { - Map<String, String> claims = authzToken.getClaimsMap(); - String userId = claims.get(Constants.USER_NAME); - String gw = claims.getOrDefault(Constants.GATEWAY_ID, gatewayId); - return new RequestContext(userId, gw, authzToken.getAccessToken(), claims); - } -} -``` - -- [ ] **Step 2: Verify compilation** - -```bash -cd /Users/yasith/code/artisan/airavata && mvn compile -pl airavata-api -``` - -Expected: BUILD SUCCESS. - -- [ ] **Step 3: Commit** - -```bash -git add airavata-api/src/main/java/org/apache/airavata/api/server/handler/ThriftAdapter.java -git commit -m "feat: add ThriftAdapter for DRY exception translation" -``` - ---- - -### Task 8: Rewire AiravataServerHandler experiment methods - -**Files:** -- Modify: `airavata-api/src/main/java/org/apache/airavata/api/server/handler/AiravataServerHandler.java` - -The handler gains an `ExperimentService` field. Experiment methods become one-liner delegates via `ThriftAdapter`. - -- [ ] **Step 1: Add ExperimentService field and update constructor** - -Add import at the top of `AiravataServerHandler.java`: - -```java -import org.apache.airavata.service.experiment.ExperimentService; -import org.apache.airavata.service.messaging.EventPublisher; -``` - -Add field after line 112 (`private Publisher experimentPublisher;`): - -```java -private final ExperimentService experimentService; -``` - -In the 3-arg constructor (lines 118-137), after line 127 (`experimentPublisher = MessagingFactory.getPublisher(Type.EXPERIMENT_LAUNCH);`), add: - -```java -EventPublisher eventPub = new EventPublisher(statusPublisher, experimentPublisher); -this.experimentService = new ExperimentService(registryHandler, sharingHandler, eventPub); -``` - -- [ ] **Step 2: Rewire createExperiment** - -Replace the body of `createExperiment` (lines 1417-1472) with: - -```java -@Override -@SecurityCheck -public String createExperiment(AuthzToken authzToken, String gatewayId, ExperimentModel experiment) - throws InvalidRequestException, AiravataClientException, AiravataSystemException, - AuthorizationException, TException { - return ThriftAdapter.execute(authzToken, gatewayId, - ctx -> experimentService.createExperiment(ctx, experiment)); -} -``` - -- [ ] **Step 3: Rewire deleteExperiment** - -Replace lines 1487-1528: - -```java -@Override -@SecurityCheck -public boolean deleteExperiment(AuthzToken authzToken, String experimentId) - throws InvalidRequestException, AiravataClientException, AiravataSystemException, - AuthorizationException, TException { - return ThriftAdapter.execute(authzToken, null, - ctx -> experimentService.deleteExperiment(ctx, experimentId)); -} -``` - -- [ ] **Step 4: Rewire getExperiment** - -Replace lines 1554-1594: - -```java -@Override -@SecurityCheck -public ExperimentModel getExperiment(AuthzToken authzToken, String airavataExperimentId) - throws InvalidRequestException, ExperimentNotFoundException, AiravataClientException, - AiravataSystemException, AuthorizationException, TException { - return ThriftAdapter.execute(authzToken, null, - ctx -> experimentService.getExperiment(ctx, airavataExperimentId)); -} -``` - -- [ ] **Step 5: Rewire getExperimentByAdmin** - -Replace lines 1598-1617: - -```java -@Override -@SecurityCheck -public ExperimentModel getExperimentByAdmin(AuthzToken authzToken, String airavataExperimentId) - throws InvalidRequestException, ExperimentNotFoundException, AiravataClientException, - AiravataSystemException, AuthorizationException, TException { - return ThriftAdapter.execute(authzToken, null, - ctx -> experimentService.getExperimentByAdmin(ctx, airavataExperimentId)); -} -``` - -- [ ] **Step 6: Rewire searchExperiments** - -Replace lines 1134-1235: - -```java -@Override -@SecurityCheck -public List<ExperimentSummaryModel> searchExperiments( - AuthzToken authzToken, String gatewayId, String userName, - Map<ExperimentSearchFields, String> filters, int limit, int offset) - throws InvalidRequestException, AiravataClientException, AiravataSystemException, - AuthorizationException, TException { - return ThriftAdapter.execute(authzToken, gatewayId, - ctx -> experimentService.searchExperiments(ctx, gatewayId, userName, filters, limit, offset)); -} -``` - -- [ ] **Step 7: Rewire getExperimentStatus** - -Replace lines 1845-1854: - -```java -@Override -@SecurityCheck -public ExperimentStatus getExperimentStatus(AuthzToken authzToken, String airavataExperimentId) - throws TException { - return ThriftAdapter.execute(authzToken, null, - ctx -> experimentService.getExperimentStatus(ctx, airavataExperimentId)); -} -``` - -- [ ] **Step 8: Rewire getExperimentOutputs** - -Replace lines 1858-1870: - -```java -@Override -@SecurityCheck -public List<OutputDataObjectType> getExperimentOutputs(AuthzToken authzToken, String airavataExperimentId) - throws AuthorizationException, TException { - return ThriftAdapter.execute(authzToken, null, - ctx -> experimentService.getExperimentOutputs(ctx, airavataExperimentId)); -} -``` - -- [ ] **Step 9: Rewire terminateExperiment** - -Replace lines 2413-2453: - -```java -@Override -@SecurityCheck -public void terminateExperiment(AuthzToken authzToken, String airavataExperimentId, String gatewayId) - throws TException { - ThriftAdapter.executeVoid(authzToken, gatewayId, - ctx -> experimentService.terminateExperiment(ctx, airavataExperimentId)); -} -``` - -- [ ] **Step 10: Rewire cloneExperiment** - -Replace lines 2250-2268: - -```java -@Override -@SecurityCheck -public String cloneExperiment( - AuthzToken authzToken, String existingExperimentID, - String newExperimentName, String newExperimentProjectId) - throws InvalidRequestException, ExperimentNotFoundException, AiravataClientException, - AiravataSystemException, AuthorizationException, ProjectNotFoundException, TException { - return ThriftAdapter.execute(authzToken, null, - ctx -> experimentService.cloneExperiment( - ctx, existingExperimentID, newExperimentName, newExperimentProjectId, false)); -} -``` - -- [ ] **Step 11: Rewire cloneExperimentByAdmin** - -Replace lines 2272-2290: - -```java -@Override -@SecurityCheck -public String cloneExperimentByAdmin( - AuthzToken authzToken, String existingExperimentID, - String newExperimentName, String newExperimentProjectId) - throws InvalidRequestException, ExperimentNotFoundException, AiravataClientException, - AiravataSystemException, AuthorizationException, ProjectNotFoundException, TException { - return ThriftAdapter.execute(authzToken, null, - ctx -> experimentService.cloneExperiment( - ctx, existingExperimentID, newExperimentName, newExperimentProjectId, true)); -} -``` - -- [ ] **Step 12: Remove cloneExperimentInternal private method** - -Delete the `cloneExperimentInternal` method (lines 2292-2370) since its logic now lives in `ExperimentService.cloneExperiment()`. - -- [ ] **Step 13: Check if submit helpers can be removed** - -```bash -grep -n "submitExperiment\|submitCancelExperiment\|submitExperimentIntermediateOutputsEvent" \ - airavata-api/src/main/java/org/apache/airavata/api/server/handler/AiravataServerHandler.java -``` - -If `submitExperiment` is only called from `launchExperiment` (which we haven't migrated yet — it's complex and has `getGroupResourceList` and `getApplicationInterface` dependencies), leave it for now. Same for `submitCancelExperiment` and `submitExperimentIntermediateOutputsEvent`. These can be cleaned up when `launchExperiment` is migrated. - -- [ ] **Step 14: Verify compilation** - -```bash -cd /Users/yasith/code/artisan/airavata && mvn compile -pl airavata-api -``` - -Expected: BUILD SUCCESS. - -- [ ] **Step 15: Commit** - -```bash -git add airavata-api/src/main/java/org/apache/airavata/api/server/handler/AiravataServerHandler.java -git commit -m "refactor: rewire AiravataServerHandler experiment methods to ExperimentService" -``` - ---- - -### Task 9: Run full test suite and verify - -**Files:** None (verification only) - -- [ ] **Step 1: Run all tests** - -```bash -cd /Users/yasith/code/artisan/airavata && mvn test -``` - -Expected: All tests pass. The Thrift interface is unchanged — callers see no difference. - -- [ ] **Step 2: Run service layer tests independently** - -```bash -cd /Users/yasith/code/artisan/airavata && mvn test -pl airavata-api -Dtest="org.apache.airavata.service.**" -``` - -Expected: All ExperimentServiceTest and RequestContextTest pass. - -- [ ] **Step 3: Fix and commit any test failures** - -If tests fail, fix the issues and commit. If all tests pass, no commit needed. diff --git a/docs/superpowers/specs/2026-03-26-airavata-service-layer-design.md b/docs/superpowers/specs/2026-03-26-airavata-service-layer-design.md deleted file mode 100644 index aa6fb08942..0000000000 --- a/docs/superpowers/specs/2026-03-26-airavata-service-layer-design.md +++ /dev/null @@ -1,163 +0,0 @@ -# airavata-service: Transport-Agnostic Service Layer - -## Goal - -Extract business logic from Thrift handlers into a standalone `airavata-service` Maven module so that multiple transport layers (Thrift, REST, gRPC) can share the same business logic without duplication. - -## Current State - -- `AiravataServerHandler` (~6,800 lines) implements `Airavata.Iface` with business logic, auth checks, messaging, and exception translation mixed together. -- Other handlers (`RegistryServerHandler`, `SharingRegistryServerHandler`, `CredentialStoreServerHandler`, `OrchestratorServerHandler`, profile handlers) follow the same pattern. -- `AiravataServerHandler` composes other handlers via direct Java method calls. -- A clean repository layer already exists under `registry/core/repositories/`. - -## Architecture - -### Module Layout - -``` -airavata/ - airavata-service/ # NEW - pom.xml - src/main/java/org/apache/airavata/service/ - context/RequestContext.java - exception/ServiceException.java - exception/AuthorizationException.java - exception/NotFoundException.java - experiment/ExperimentService.java - registry/RegistryService.java - sharing/SharingService.java - credential/CredentialService.java - orchestrator/OrchestratorService.java - profile/UserProfileService.java - profile/TenantProfileService.java - profile/GroupManagerService.java - profile/IamAdminService.java - messaging/EventPublisher.java - airavata-api/ # EXISTING - becomes thin Thrift transport - airavata-rest-server/ # FUTURE - airavata-grpc-server/ # FUTURE -``` - -### Dependency Flow - -``` -airavata-thrift-server (airavata-api) --\ -airavata-rest-server (future) ----> airavata-service --> registry-core (repositories) -airavata-grpc-server (future) --/ -``` - -### RequestContext - -Transport-agnostic identity object. Each transport constructs it from its own auth mechanism. - -```java -public class RequestContext { - private String userId; - private String gatewayId; - private String accessToken; - private Map<String, String> claims; - - public static RequestContext from(AuthzToken authzToken, String gatewayId) { ... } - // Future: fromBearer(), fromGrpcContext() -} -``` - -### Service Classes - -Concrete classes, no interfaces. Constructor-injected dependencies. Services call other services directly (same JVM). - -```java -public class ExperimentService { - private final ExperimentRepository experimentRepo; - private final SharingService sharingService; - private final EventPublisher eventPublisher; - - public ExperimentService(ExperimentRepository experimentRepo, - SharingService sharingService, - EventPublisher eventPublisher) { ... } - - public String createExperiment(RequestContext ctx, ExperimentModel experiment) { ... } - public void deleteExperiment(RequestContext ctx, String experimentId) { ... } - public ExperimentModel getExperiment(RequestContext ctx, String experimentId) { ... } - // ... 1-1 mapping from handler methods -} -``` - -Service dependency graph: -``` -ExperimentService --> SharingService, RegistryService, EventPublisher -RegistryService --> SharingService, EventPublisher -OrchestratorService --> RegistryService, CredentialService -``` - -### Exceptions - -Services throw their own exception types: -- `ServiceException` — general errors -- `AuthorizationException` — permission denied -- `NotFoundException` — resource not found - -Transport layers translate these to protocol-specific errors. - -### EventPublisher - -Wraps the current `MessagingFactory` / RabbitMQ publishing scattered across handlers into a single injectable dependency. - -### Transport Adapters (DRY) - -One utility class per transport eliminates repeated try/catch and RequestContext construction: - -```java -public class ThriftAdapter { - public static <T> T execute(AuthzToken authzToken, String gatewayId, - ServiceCall<T> call) - throws AiravataSystemException, AuthorizationException { - try { - RequestContext ctx = RequestContext.from(authzToken, gatewayId); - return call.apply(ctx); - } catch (ServiceException e) { - throw new AiravataSystemException(e.getMessage()); - } catch (org.apache.airavata.service.exception.AuthorizationException e) { - throw new AuthorizationException(e.getMessage()); - } - } - - @FunctionalInterface - public interface ServiceCall<T> { T apply(RequestContext ctx) throws Exception; } -} -``` - -Handler methods become one-liners: -```java -@Override -public String createExperiment(AuthzToken token, String gatewayId, - ExperimentModel experiment) - throws AiravataSystemException, AuthorizationException { - return ThriftAdapter.execute(token, gatewayId, - ctx -> experimentService.createExperiment(ctx, experiment)); -} -``` - -### Auth - -- `@SecurityCheck` annotation removed from handlers. -- Transport-specific middleware validates credentials and populates `RequestContext` before handler methods run. -- Services perform authorization checks (e.g., "does this user have access?") using `RequestContext`. - -### Model Objects - -Services use the existing Thrift-generated model classes (`ExperimentModel`, `ProjectModel`, etc.) as-is. They're plain Java POJOs. Replacing them with hand-written domain objects is a separate concern. - -## Migration Strategy (Incremental) - -1. **Create `airavata-service` module** with `RequestContext`, exception types, `EventPublisher`. -2. **Extract `ExperimentService`** from `AiravataServerHandler` experiment-related methods: - - `createExperiment`, `deleteExperiment`, `getExperiment`, `getExperimentByAdmin` - - `getExperimentList`, `searchExperiments` - - `launchExperiment`, `cancelExperiment`, `cloneExperiment` - - `getExperimentStatus`, `getExperimentOutputs`, `getIntermediateOutputs` -3. **Create `ThriftAdapter`** in `airavata-api`. Rewire handler experiment methods to delegate through adapter. -4. **Verify existing tests pass.** Thrift interface is unchanged. -5. **Repeat** for remaining domains: Registry, Sharing, Credential, Orchestrator, Profile services. -6. **REST and gRPC servers** come later as separate modules once the service layer is proven.
