# iComposer Hooks User Guide

> This guide is for business developers writing **Hook** scripts on the iComposer platform. It covers the concepts, authoring workflow, and verification methods for the three types of Hooks.

## 1. Overview: What are Hooks?

iComposer Hooks are a **hot-deployable Groovy script extension mechanism** that lets tenants inject **long-lived, runtime-resident** custom logic without restarting the service:

- Listen to messages (iComposer Listener)
- Participate in distributed transactions (TCC)
- React to cluster-wide cache changes (Clear Cache Listener)

Unlike regular Groovy APIs / ServiceFunctions (request-driven and executed only when invoked), a Hook **continuously listens for events** after registration and is triggered automatically when an event arrives.

### Key characteristics

- **Script-based**: Write Groovy method bodies in the UI editor; saving compiles immediately.
- **Hot reload**: After a new version is published, no restart is needed — the platform automatically unloads the old script and loads the new one via cache events.
- **Tenant-isolated**: Each Hook belongs to a tenant/module and is isolated by tenant context at registration time.
- **Auto-recovery on startup**: When the runtime service restarts, it automatically pulls all active Hooks from the database and re-registers them.

## 2. The three Hook types at a glance

| Type | Category ID | Handler interface | When triggered | Typical scenario |
|---|---|---|---|---|
| iComposer Listener | 1 | `ListenerHandler` | MQ message arrival (Kafka / RabbitMQ) | Asynchronously consume order / notification queues |
| TCC | 2 | `TccHandler` | Seata global transaction prepare / commit / rollback | Cross-service distributed transactions: reserve / confirm / roll back resources |
| Clear Cache Listener | 3 | `ClearCacheHandler` | Cluster-wide cache change event | Clear local caches when upstream configuration changes |

## 3. Authoring and deploying Hooks in the UI

All three Hook types share the same authoring and deployment flow — learn it once.

### 3.1 Entry point and steps

1. Open the iComposer designer and go to the **Hooks management page**.
2. Create a new hook and **choose a category**:
   - `1` → iComposer Listener
   - `2` → TCC
   - `3` → Clear Cache Listener
3. After choosing a category, the editor **automatically applies the corresponding code template** as a starting point.
4. Write/modify the Groovy method body in the "user code area".
5. Save and set the status to **ACTIVE**; the platform immediately triggers compilation.

### 3.2 Built-in capabilities available in the script

`BaseMonitorGroovySupport` provides the following out-of-the-box methods for every Hook:

| Purpose | Call |
|---|---|
| Get a Spring Bean | `getBean("beanName")` or `getBean(SomeType.class)` |
| Logging | `info(...)` / `warn(...)` / `error(...)` / `debug(...)` |

In addition, utility classes under `com.insuremo.icomposer.utils` (such as `IComposerHooksUtils`, `IComposerEnv`) can be imported and used directly in scripts. See each type's section below for details.

## 4. Type 1: iComposer Listener tutorial

### 4.1 Responsibility

Listen continuously (Kafka / RabbitMQ) and hand each message off to a ServiceFunction for processing.

### 4.2 Interface and methods

| Method | When called | What you do |
|---|---|---|
| `void init()` | On application startup / after hot-reload registration | Read MQ config, create the Listener container, register it with the Registry |
| `void destroy()` | On unregister / shutdown / hot-reload unload | Release custom resources |

> You may also define extra methods (e.g. `send(data)` as a companion producer); the platform will not interfere.

### 4.3 When triggered

Consumption starts automatically after the Spring container starts; **each time a message arrives** your message callback is triggered.

### 4.4 Three-step authoring

```text
① Read config   IComposerEnv.getParameter("mq.<business>.xxx")
② Build container   Build a ConnectionFactory / ConsumerFactory + Listener container;
            in the message callback, use IComposerHooksUtils.executeMethod(functionName, methodName, body)
            to hand the message off to a ServiceFunction
③ Register     IComposerHooksUtils.registerResource(this, [container: container])
```

Key points:
- **Lifecycle ordering**: Before calling your `destroy()`, the platform first calls `stop()` on any `container` registered via `registerResource` (waiting for in-flight messages to finish processing). Therefore `destroy()` only needs to release custom resources outside the container (e.g. a self-created `CachingConnectionFactory`).
- `registerResource(this, [container: container])` automatically extracts and manages the `container` (a Spring `Lifecycle`); the **resource ID** is your class name with the `Gry_Monitor_` prefix removed.
- For `executeMethod(functionName, methodName, args...)`, when `methodName` is `null` it defaults to `execute`.
- Always `try/catch` inside the message callback and log exceptions with `error(...)` to avoid silently swallowing them.
- Multiple MQ connections: each MonitorTask script uses its own `mq.<business>.*` config-center parameters, independent of one another.

### 4.5 Full example: Kafka Listener

```groovy
import org.springframework.kafka.listener.ConcurrentMessageListenerContainer
import org.springframework.kafka.listener.ContainerProperties
import org.springframework.kafka.core.DefaultKafkaConsumerFactory
import com.insuremo.icomposer.utils.IComposerHooksUtils
import com.insuremo.icomposer.utils.IComposerEnv

private ConcurrentMessageListenerContainer container

void init() {
    // 1. Read Kafka connection params from the config center (do not hardcode)
    def bootstrapServers = IComposerEnv.getParameter("mq.claim.bootstrapServers")
    def topic            = IComposerEnv.getParameter("mq.claim.topic")
    def groupId          = IComposerEnv.getParameter("mq.claim.groupId")
    def consumers        = IComposerEnv.getParameter("mq.claim.consumers", "3") as int

    // 2. Build the ConsumerFactory + Listener container
    def props = new HashMap<String, Object>()
    props.put("bootstrap.servers", bootstrapServers)
    props.put("group.id", groupId)
    props.put("key.deserializer",   "org.apache.kafka.common.serialization.StringDeserializer")
    props.put("value.deserializer", "org.apache.kafka.common.serialization.StringDeserializer")
    props.put("auto.offset.reset",  "latest")
    def consumerFactory = new DefaultKafkaConsumerFactory<String, String>(props)

    def containerProps = new ContainerProperties(topic)
    containerProps.setGroupId(groupId)

    container = new ConcurrentMessageListenerContainer(consumerFactory, containerProps)
    container.getContainerProperties().setMessageListener({ record ->
        try {
            // Hand the message off to a ServiceFunction for processing
            IComposerHooksUtils.executeMethod("ClaimHandlerFunction", "execute", record.value())
        } catch (Exception e) {
            error("Failed to process message: ${e.message}"); e.printStackTrace()
        }
    })
    container.setConcurrency(consumers)

    // 3. Register with the Registry (container is managed automatically)
    IComposerHooksUtils.registerResource(this, [container: container])
    info("Kafka ClaimListenerHandler started")
}

void destroy() {
    // The framework already called lifecycle.stop() to wait for in-flight messages;
    // release only custom resources here
    info("Kafka ClaimListenerHandler destroyed")
}
```

> Configure in the config center: `mq.claim.bootstrapServers`, `mq.claim.topic`, `mq.claim.groupId`, `mq.claim.consumers`.

### 4.6 Variant: RabbitMQ Listener

Replace the Kafka parts of the example above with RabbitMQ; the structure is identical:

```groovy
import org.springframework.amqp.rabbit.connection.CachingConnectionFactory
import org.springframework.amqp.rabbit.listener.SimpleMessageListenerContainer
import com.insuremo.icomposer.utils.IComposerHooksUtils
import com.insuremo.icomposer.utils.IComposerEnv

private CachingConnectionFactory connectionFactory
private SimpleMessageListenerContainer container

void init() {
    def host        = IComposerEnv.getParameter("mq.order.host")
    def port        = IComposerEnv.getParameter("mq.order.port", "5672") as int
    def username    = IComposerEnv.getParameter("mq.order.username")
    def password    = IComposerEnv.getParameter("mq.order.password")
    def virtualHost = IComposerEnv.getParameter("mq.order.virtualHost", "/")
    def queueName   = IComposerEnv.getParameter("mq.order.queue")
    def consumers   = IComposerEnv.getParameter("mq.order.consumers", "3") as int

    connectionFactory = new CachingConnectionFactory(host, port)
    connectionFactory.setUsername(username)
    connectionFactory.setPassword(password)
    connectionFactory.setVirtualHost(virtualHost)

    container = new SimpleMessageListenerContainer()
    container.setConnectionFactory(connectionFactory)
    container.setQueueNames(queueName)
    container.setConcurrentConsumers(consumers)
    container.setMessageListener({ message ->
        try {
            def body = new String(message.getBody(), "UTF-8")
            IComposerHooksUtils.executeMethod("OrderHandlerFunction", "execute", body)
        } catch (Exception e) {
            error("Failed to process message: ${e.message}"); e.printStackTrace()
        }
    })

    IComposerHooksUtils.registerResource(this, [container: container])
    info("RabbitMQ OrderListenerHandler started")
}

void destroy() {
    if (connectionFactory != null) {
        connectionFactory.destroy()
    }
    info("RabbitMQ OrderListenerHandler destroyed")
}
```

> Configure in the config center: `mq.order.host`, `mq.order.port`, `mq.order.username`, `mq.order.password`, `mq.order.virtualHost`, `mq.order.queue`, `mq.order.consumers`.

### 4.7 How to verify

1. After saving the script (ACTIVE), make sure the application has started or finished hot-reloading.
2. Send a test message to the target queue / topic.
3. Check the application logs: you should see the `started` log line, plus the ServiceFunction's execution log when a message arrives.

## 5. Type 2: TCC distributed transaction tutorial

### 5.1 Responsibility

Act as a **branch participant** in a Seata TCC (Try-Confirm-Cancel) distributed transaction, reserving resources in the Try phase and letting the transaction coordinator decide the final commit or rollback.

### 5.2 Interface and methods

| Method | Phase | Return | Description |
|---|---|---|---|
| `Boolean prepare(IComposerTccActionContext ctx)` | Try | `Boolean` | Reserve resources (freeze an amount, lock stock, etc.) |
| `Boolean commit(IComposerTccActionContext ctx)` | Confirm | `Boolean` | Confirm resources (deduct, confirm payment, etc.) |
| `Boolean rollback(IComposerTccActionContext ctx)` | Cancel | `Boolean` | Release the reserved resources |

> The return type is **`Boolean` (the wrapper type)**, not the primitive `boolean`. Returning `true` means success; throwing an exception or returning `false` means failure.

### 5.3 When triggered

- **prepare**: Triggered explicitly by **business code** calling `IComposerHooksUtils.prepareTcc("actionName", params)`.
- **commit / rollback**: Coordinated and called by the **Seata TC (transaction coordinator)** when the global transaction ends; **not called directly by business code**.

### 5.4 Key API: IComposerTccActionContext

| Method | Return | Description |
|---|---|---|
| `getParams()` | `String` | The business-parameter JSON passed in during prepare |
| `getXid()` | `String` | Global transaction ID |
| `getActionName()` | `String` | Name of the current TCC action |

### 5.5 Authoring tips

- Resources reserved in prepare **must** be confirmed in commit and released in rollback — the three must agree.
- **commit / rollback must be idempotent**: Seata may retry these two methods due to timeouts or other reasons.
- `getParams()` returns a JSON string; parse it yourself as needed.

### 5.6 Full example: freeze / confirm / unfreeze an amount

```groovy
import com.insuremo.icomposer.hooks.tcc.IComposerTccActionContext
import com.insuremo.icomposer.utils.IComposerJsonUtils

// Try: freeze the amount
Boolean prepare(IComposerTccActionContext ctx) {
    def params = new IComposerJsonUtils().parseText(ctx.getParams())
    def xid    = ctx.getXid()
    info("TCC prepare: xid=${xid}, orderId=${params.orderId}, amount=${params.amount}")

    // Business logic: validate and freeze the amount
    // orderService.freezeAmount(params.orderId as String, params.amount as BigDecimal)
    return true
}

// Confirm: confirm the deduction
Boolean commit(IComposerTccActionContext ctx) {
    def xid = ctx.getXid()
    info("TCC commit: xid=${xid}, action=${ctx.getActionName()}")

    // Business logic: confirm-deduct the amount frozen in prepare (must be idempotent)
    // (prepare usually records the freeze info keyed by xid; confirm accordingly)
    return true
}

// Cancel: unfreeze and release
Boolean rollback(IComposerTccActionContext ctx) {
    def xid = ctx.getXid()
    info("TCC rollback: xid=${xid}, action=${ctx.getActionName()}")

    // Business logic: release the amount frozen in prepare (must be idempotent)
    // (prepare usually records the freeze info keyed by xid; release accordingly)
    return true
}
```

How business code triggers prepare (written in another api/ServiceFunction):

```groovy
import com.insuremo.icomposer.utils.IComposerHooksUtils

def params = [orderId: "ORD-001", amount: 100]
boolean ok = IComposerHooksUtils.prepareTcc("FreezeAmountAction", params)
// ok == true means prepare succeeded; commit / rollback are then coordinated by Seata TC when the global transaction ends
```

### 5.7 How to verify

1. Issue a business request that calls `prepareTcc("FreezeAmountAction", ...)`.
2. **Success path**: all branches' prepare succeed → observe the `prepare` and `commit` logs.
3. **Failure path**: deliberately make one branch's prepare fail or throw → observe the `prepare` and `rollback` logs.

## 6. Type 3: ClearCache cache-event tutorial

### 6.1 Responsibility

Listen for the platform's **cluster-wide cache change events**. When another node publishes a configuration / product / rule change (triggering cluster-wide cache sync), this node clears or refreshes its locally derived caches accordingly.

### 6.2 Interface and methods

Implement `com.insuremo.unicorn.platform.groovy.monitor.cache.clear.ClearCacheHandler`:

```groovy
void onCacheChanged(IComposerCacheChangeEvent event,
                    String bucketName,
                    String cacheName,
                    Object cacheKey)
```

| Parameter | Meaning |
|---|---|
| `event` | Cache change event; carries tenant, change type, etc. |
| `bucketName` | Cache bucket name |
| `cacheName` | Cache name |
| `cacheKey` | The changed cache key; **`null` means clear-all** |

### 6.3 When triggered

After the iComposer service receives a platform cache-sync event, it **dispatches to all registered ClearCacheHandlers**. Triggered on every cache change.

### 6.4 Key API: IComposerCacheChangeEvent

| Method | Description |
|---|---|
| `getType()` | Change type (e.g. `CLEAR` for clear-all / `UPDATE` for a single entry) |
| `getTenantCode()` | Tenant code |

### 6.5 Authoring tips

- `onCacheChanged` receives **all** cache change events; filter by `bucketName` / `cacheName` for the ones you care about.
- `cacheKey == null` means clear-all; in that case you should refresh the entire local cache.
- A single handler throwing an exception **does not** affect other handlers (the dispatcher calls them one by one and isolates failures).

### 6.6 Full example: clear locally derived caches by cache name

```groovy
import com.insuremo.icomposer.hooks.cache.clear.IComposerCacheChangeEvent

void onCacheChanged(IComposerCacheChangeEvent event,
                    String bucketName,
                    String cacheName,
                    Object cacheKey) {
    info("Cache changed: bucket=${bucketName}, cache=${cacheName}, key=${cacheKey}, type=${event.getType()}, tenant=${event.getTenantCode()}")

    if (cacheKey == null) {
        // Clear-all
        // productLocalCache.invalidateAll()
        return
    }
    // Branch by cache name
    if (cacheName.contains("ProductCache")) {
        // productLocalCache.invalidate(cacheKey)
    } else if (cacheName.contains("RuleCache")) {
        // ruleEngine.reload(cacheKey)
    }
}
```


## 7. FAQ & troubleshooting

**Q1: Saved but not taking effect?**
Check whether the status is ACTIVE; check whether compilation reported errors (the return info when saving); confirm the runtime has hot-reloaded (use the `status` API in 8.2 to see whether the resource is registered). If necessary, bump the version number and save again to trigger cache invalidation.

**Q2: Should I write a full class, or just the method body?**
Write only a **method-body fragment** (imports + methods). The platform's `MonitorTaskBeanGenerator` automatically wraps it in a `class Gry_Monitor_<name> extends BaseMonitorGroovySupport implements <corresponding Handler>` shell. Adding an outer `class` yourself will instead cause a conflict.

**Q3: How do I get a Spring Bean / log in a script?**
Use `getBean("xxx")` or `getBean(SomeType.class)` to get a Bean; use `info(...)` / `warn(...)` / `error(...)` / `debug(...)` for logging — these are provided by `BaseMonitorGroovySupport`.

**Q4: What if one Hook needs to connect to multiple MQs?**
Give each MonitorTask script its own `mq.<business>.*` config-center parameters; each creates its own connection inside the script, independently.

**Q5: TCC commit / rollback not being called?**
Check: ① did prepare return `true`; ② is Seata TC running normally; ③ confirm that commit/rollback is coordinated by TC, **not** called directly by business code.

**Q6: What happens if Listener consumption throws an exception?**
`try/catch` inside the message callback and log it with `error(...)`. Before `destroy()`, the framework first calls `lifecycle.stop()` to wait for in-flight messages to finish — it will not abort abruptly.

**Q7: Too many events in onCacheChanged?**
This is normal — it receives every cache change. Filter by `bucketName` / `cacheName` inside the method; `cacheKey == null` means clear-all.

## Appendix: Cheat sheet for the three Hook types

| Dimension | iComposer Listener | TCC | Clear Cache Listener |
|---|---|---|---|
| Category ID | 1 | 2 | 3 |
| Handler interface | `ListenerHandler` | `TccHandler` | `ClearCacheHandler` |
| Key methods | `init()` / `destroy()` | `prepare` / `commit` / `rollback(ctx)` returning `Boolean` | `onCacheChanged(event, bucketName, cacheName, cacheKey)` |
| Triggered by | Spring MQ container (message arrival) | Business calls `IComposerHooksUtils.prepareTcc` + Seata TC coordination | `IComposerCacheChangeListener` dispatch |
| Register / trigger API | `IComposerHooksUtils.registerResource(...)` | `IComposerHooksUtils.prepareTcc(...)` | Auto-dispatched by the platform; no manual registration |

---
