# Script Body Guide (Groovy Java)

The script acts as a **boolean filter**:

* Return `true` → the event is significant and an alert should be generated
* Return `false` → suppress the alert for this event

***

### 1) Script Body rules (must-follow) <a href="#id-1-script-body-rules-must-follow" id="id-1-script-body-rules-must-follow"></a>

* **Groovy only**
* The Script Body is the **inside of a method** \
  ✅ Write statements directly \
  ❌ Do not add a class or method signature
* **Always return a boolean** on every path
* Prefer **early `return false`** for readability and safety

***

### 2) Runtime variables (available without declaration) <a href="#id-2-runtime-variables-available-without-declaration" id="id-2-runtime-variables-available-without-declaration"></a>

These are injected into the script context automatically:

#### **Core context**

* **`dlpObject` (Object)** Current state of the object that triggered the event.
* **`oldDlpObject` (Object)** Previous state of the object **only for UPDATE** events. Often `null` on INSERT, and may not match expected type.
* **`wsUser` (User)** The user/system account that performed the action. Useful for role-based suppression.

#### **Message output**

* **`mapResultsForMessage` (Map\<String, String>)** Placeholder map for template-based message text. Typical keys are `"?1"`, `"?2"` etc.
* **`mapMessageFromServer` (Map\<String, String>)** Full message override. Use key `"MSG"` to provide the message text.

***

### 3) Recommended structure (copy-paste mental model) <a href="#id-3-recommended-structure-copy-paste-mental-model" id="id-3-recommended-structure-copy-paste-mental-model"></a>

A clean script usually follows this flow:

1. **Imports** (only what you use)
2. **Type check + cast** `dlpObject` (and optionally `oldDlpObject`)
3. **Guard clauses** for nulls / wrong event shape
4. **Business logic** (status checks, threshold checks, role checks, etc.)
5. **Populate message values** (template placeholders or full message)
6. `return true`

***

### 4) Type check + casting (avoid ClassCastException) <a href="#id-4-type-check--casting-avoid-classcastexception" id="id-4-type-check--casting-avoid-classcastexception"></a>

`dlpObject` and `oldDlpObject` are generic `Object`, so type-check first.

```groovy
import com.dataloy.ds.Voyage
if (!(dlpObject instanceof Voyage)) 
    return false Voyage current = (Voyage) dlpObject
```

For old object (updates only):

```groovy
Voyage old = 
    (oldDlpObject instanceof Voyage)
     ? (Voyage) oldDlpObject
     : null
```

***

### 5) Null-Safety: Patterns and Best Practices <a href="#id-5-null-safety-patterns-dont-catch-npes-with-your-face" id="id-5-null-safety-patterns-dont-catch-npes-with-your-face"></a>

Assume linked objects can be null (headers, vessel, statuses, etc.). Use:

* **Guard clauses**
* Groovy **safe navigation** `?.`
* Null coalescing `?:`

```groovy
def vesselName = vessel?.vesselName
// vessel is null, so "Unknown vessel" is returned
def displayName = vesselName ?: "Unknown vessel"

def refNo = current?.voyageHeader?.referenceNo 
if (!refNo) 
    return false
```

***

### 6) Field access and status comparisons

#### Linked object navigation

```groovy
def statusCode = current?.voyageHeader?.voyageStatus?.statusTypeCode
```

#### Status fields

Compare **status codes**, not the status object:&#x20;

```groovy
if (!"OPR".equals(statusCode)) 
    return false
```

{% hint style="info" %}
Tip: `"LITERAL".equals(x)` is null-safe and avoids surprises.
{% endhint %}

***

### 7) Update/change detection (current vs old) <a href="#id-7-update-change-detection-current-vs-old" id="id-7-update-change-detection-current-vs-old"></a>

If your trigger is UPDATE and you only want alerts on change:

#### **Step-by-step pattern**

```groovy
if (old == null) return false // not an update or no previous state
```

#### **Null-safe comparisons**

Avoid calling `.equals()` on potentially null values.

```groovy
def newVal = current?.someField 
def oldVal = old?.someField
if (newVal == null || oldVal == null) return false 
if (newVal == oldVal) return false 
```

#### **Numeric thresholds (example)**

```groovy
import java.math.BigDecimal
BigDecimal newPnl = current?.voyageResult as BigDecimal 
BigDecimal oldPnl = old?.voyageResult as BigDecimal 
if (newPnl == null || oldPnl == null) return false
if ((newPnl - oldPnl).abs() <= 20000G) return false
```

***

### 8) Message building modes (CRITICAL – prevents misconfiguration)

There are **two valid combinations** between the Script Body and Message Configuration.

#### **Mode A — Build message in script = true**

Use this when the Script Body produces the full message text.**Requirements**

* `Build message in script = true`
* Script must set: `mapMessageFromServer["MSG"]`

{% code overflow="wrap" %}

```groovy
mapMessageFromServer.put("MSG", "Voyage PNL changed: " + current?.voyageHeader?.referenceNo) 
return true
```

{% endcode %}

#### **Mode B — Build message in script = false**

Use this when the server builds the message using a template string (`messageTxt`) and placeholder map.

**Requirements**

* `Build message in script = false`
* `messageTxt` must be provided (example: `"Hello ?1"`)
* Script sets: `mapResultsForMessage["?1"] = "World"`

Example configuration:

* `messageTxt = "Hello ?1"`

**Script:**

```groovy
mapResultsForMessage.put("?1", "World")
return true
```

#### **The broken combo**

**Don’t do this:**

* `mapResultsForMessage` + `Build message in script = true`

Reason: `Build message in script = true` expects `"MSG"` in `mapMessageFromServer`.

***

### 9) Guidance for generators / validation logic (recommended) <a href="#id-9-guidance-for-generators-validation-logic-recommended" id="id-9-guidance-for-generators-validation-logic-recommended"></a>

When auto-generating alert script configs, set `Build message in script` using this rule:

1. If Script Body contains `mapMessageFromServer` → set **true**
2. Else if Script Body contains `mapResultsForMessage` AND `messageTxt` is set → set **false**
3. Else → fail validation or require the missing pieces (recommended)

This prevents “alerts firing with empty messages” situations.

***

### 10) Optional: fetching related data (advanced) <a href="#id-10-optional-fetching-related-data-advanced" id="id-10-optional-fetching-related-data-advanced"></a>

Sometimes the needed data is not reachable from `dlpObject` (link not loaded / not present). You can query the database using:

```groovy
com.dataloy.platform.query.DlpObjectSelect
```

{% hint style="warning" %}
Method details can vary by version and are not fully documented. Use known examples as reference and test carefully.
{% endhint %}

***

### 11) Troubleshooting <a href="#id-11-troubleshooting" id="id-11-troubleshooting"></a>

#### **Logging**

Print debug information to application logs:

```groovy
java.lang.System.out.println("---- newPnl: " + newPnl)
```

#### **Return-path sanity**

If alerts don’t fire:

* confirm type-check isn’t rejecting
* confirm `oldDlpObject` isn’t null when you expect update logic
* confirm [message building](https://app.gitbook.com/o/-LhoT2vqihl0pYiCeolt/s/7R7IFboJW2JabvnivFfc/~/edit/~/changes/32/voyage-management-system/step-by-step-guides/alerts/alert-scripts/script-body-groovy-detailed-guide#id-8-message-building-modes-critical-prevents-misconfiguration) is valid

***

### 12) Full example (safe, readable, and template-based) <a href="#id-12-full-example-safe-readable-and-template-based" id="id-12-full-example-safe-readable-and-template-based"></a>

Config:

* `Build message in script = false`
* `messageTxt = "Voyage ?1 PNL changed to ?2"`

Script:

```groovy
import com.dataloy.ds.Voyage 
import java.math.BigDecimal

if (!(dlpObject instanceof Voyage)) return false 
Voyage current = (Voyage) dlpObject

Voyage old = (oldDlpObject instanceof Voyage) ? (Voyage) oldDlpObject : null 
if (old == null) return false

BigDecimal newPnl = current?.voyageResult as BigDecimal 
BigDecimal oldPnl = old?.voyageResult as BigDecimal 
if (newPnl == null || oldPnl == null) return false

if ((newPnl - oldPnl).abs() <= 20000G) return false

mapResultsForMessage.put("?1", current?.voyageHeader?.referenceNo ?: "") 
mapResultsForMessage.put("?2", String.valueOf(newPnl))
return true
```
