You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@airavata.apache.org by ya...@apache.org on 2026/03/26 18:13:54 UTC
(airavata) 38/38: chore: remove working design docs
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 <ya...@icloud.com>
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.