Send a Data Integration Report by Email — No Pipeline Needed

Run a restricted Oracle EPM Cloud Data Integration report through a Groovy business rule and automatically email the output to the user who launched the rule.

Oracle EPM Cloud • Groovy Automation

Send a Data Integration Report by Email — No Pipeline Needed

Run a protected Data Integration report through Groovy and REST, then automatically deliver the result to the user who launched the rule.

FCCS Groovy REST API System Administrator
!

Why is this automation needed?

The TB All Columns (Per,Cat,Loc) Data Integration report can only be executed by a System Administrator. Business users may still need the output, but granting them administrative access is neither appropriate nor secure. This rule bridges the gap: the report runs through an administrator-authenticated named connection, while the generated file is delivered to the actual user who launched the Groovy rule.

At a glance

1

Protected execution

The REST request uses the administrator credentials stored in the named connection.

2

User-aware delivery

The script resolves the email address of the actual rule executor through Access Control.

3

No Pipeline dependency

No pipeline code, process lookup, proxy-user comparison, or pipeline submitter resolution is required.

Required configuration

ComponentRequired valuePurpose
Named ConnectionEPM_RESTAuthenticates the Data Integration, Access Control, and Send Mail REST calls.
ProviderOther Web Service ProviderCreates the REST connection used by the Groovy business rule.
Substitution VariableDataPushSupplies the report period, for example JUL-2026.
Runtime PromptsvDM_Category
vDM_Location
vDM_RuleName
Pass the Category, Location, and Integration Name into the report request.
Data Integration ReportTB All Columns (Per,Cat,Loc)The exact executable report definition used in the REST payload.

Setup guide

1

Create the named connection

Create a connection named EPM_REST and select Other Web Service Provider. The credentials stored in this connection must belong to a user with the rights required to execute the report and send email.

Create EPM_REST connection using Other Web Service Provider
2

Create the substitution variable

Create the application-level substitution variable DataPush. Store the period in the format expected by the Data Integration report, for example JUL-2026.

Create the DataPush substitution variable
3

Create the Runtime Prompts

Add text RTPs named vDM_Category, vDM_Location, and vDM_RuleName. These values must match the report selectors configured in Data Integration.

Execution flow

User launches rule
Resolve executor email
Read DataPush
Run report via REST
Poll job status
Email attachment
Security model: the user launching the rule does not receive System Administrator access. The privileged action is performed by the named connection, and only the resulting file is delivered to the end user.

Full Groovy script

The script below is fully displayed and can be copied directly into an Oracle EPM Cloud Groovy business rule.

DataIntegrationReport_ExecutorEmail.groovy
/*RTPS: {vDM_Category} {vDM_Location} {vDM_RuleName} */

import groovy.json.JsonOutput
import groovy.json.JsonSlurper
import oracle.epm.api.model.User

/* =========================================================
   STEP 1 - CONFIGURATION
   ========================================================= */

final String CONNECTION_NAME = "EPM_REST"
final String PERIOD_SUBVAR_NAME = "DataPush"
final String REPORT_NAME = "TB All Columns (Per,Cat,Loc)"
final String DEFAULT_REPORT_FORMAT = "XLSX"
final String EMAIL_SUBJECT_PREFIX = "Data Integration Report"

final int REPORT_MAX_POLL_COUNT = 60
final int MAIL_MAX_POLL_COUNT = 30
final long INITIAL_POLL_DELAY_MS = 1000L
final long MAX_POLL_DELAY_MS = 10000L

Map periodSelectorMap = new LinkedHashMap()
Map categorySelectorMap = new LinkedHashMap()
Map locationSelectorMap = new LinkedHashMap()
Map integrationNameSelectorMap = new LinkedHashMap()

/* =========================================================
   STEP 2 - HELPER FUNCTIONS
   ========================================================= */

String json(Object value) {
    return JsonOutput.toJson(value)
}


String stripOuterQuotes(String inputValue) {
    String value = inputValue == null ? "" : inputValue.trim()

    if (value.length() >= 2) {
        boolean doubleQuoted = value.startsWith("\"") && value.endsWith("\"")
        boolean singleQuoted = value.startsWith("'") && value.endsWith("'")

        if (doubleQuoted || singleQuoted) {
            value = value.substring(1, value.length() - 1).trim()
        }
    }

    return value
}


String requireText(Object rawValue, String label) {
    String value = rawValue == null ? "" : rawValue.toString()
    value = stripOuterQuotes(value)

    if (value.length() == 0 || value.equalsIgnoreCase("null")) {
        throwVetoException("Missing or empty value: " + label)
    }

    return value
}


String getMapText(Map sourceMap, String keyName) {
    if (sourceMap == null || keyName == null) {
        return ""
    }

    Object value = sourceMap.get(keyName)

    if (value == null) {
        return ""
    }

    return value.toString()
}


List getListFromPossibleKeys(Map sourceMap, List keyNames) {
    if (sourceMap == null) {
        return []
    }

    for (Object keyObj : keyNames) {
        String key = keyObj.toString()
        Object value = sourceMap.get(key)

        if (value instanceof List) {
            return (List) value
        }
    }

    return []
}


Map parseJsonToMap(String responseBody) {
    if (responseBody == null || responseBody.trim().length() == 0) {
        return new LinkedHashMap()
    }

    Object parsed = new JsonSlurper().parseText(responseBody)

    if (parsed instanceof Map) {
        return (Map) parsed
    }

    throwVetoException(
        "Expected a JSON object but received: " + responseBody
    )

    return new LinkedHashMap()
}


int getStatusFromMap(Map sourceMap) {
    String statusValue = getMapText(sourceMap, "status")

    if (statusValue != null && statusValue.trim().length() > 0) {
        try {
            return statusValue.toInteger()
        } catch (Exception ignored) {
            String upperStatus = statusValue.toUpperCase()

            if (upperStatus == "RUNNING" ||
                upperStatus == "PENDING" ||
                upperStatus == "STARTED" ||
                upperStatus == "PROCESSING" ||
                upperStatus == "IN_PROGRESS") {
                return -1
            }

            if (upperStatus == "SUCCESS" ||
                upperStatus == "SUCCEEDED" ||
                upperStatus == "COMPLETED") {
                return 0
            }

            if (upperStatus == "FAILED" || upperStatus == "ERROR") {
                return 1
            }
        }
    }

    String jobStatusValue = getMapText(sourceMap, "jobStatus")

    if (jobStatusValue != null && jobStatusValue.trim().length() > 0) {
        String upperJobStatus = jobStatusValue.toUpperCase()

        if (upperJobStatus == "RUNNING" ||
            upperJobStatus == "PENDING" ||
            upperJobStatus == "STARTED" ||
            upperJobStatus == "PROCESSING" ||
            upperJobStatus == "IN_PROGRESS") {
            return -1
        }

        if (upperJobStatus == "SUCCESS" ||
            upperJobStatus == "SUCCEEDED" ||
            upperJobStatus == "COMPLETED") {
            return 0
        }

        if (upperJobStatus == "FAILED" || upperJobStatus == "ERROR") {
            return 1
        }
    }

    return 999
}


String sanitizeFilePart(String inputValue) {
    String value = inputValue == null ? "" : inputValue.trim()

    value = value.replaceAll("[\\\\/:*?\"<>|]", "_")
    value = value.replaceAll("\\s+", "_")
    value = value.replaceAll("_+", "_")

    if (value.length() == 0) {
        value = "UNKNOWN"
    }

    return value
}


String getRequiredApplicationSubVarValue(String subVarName) {
    String subVarValue = ""

    try {
        subVarValue = operation.application.getSubstitutionVariableValue(subVarName)
    } catch (Exception e) {
        throwVetoException(
            "Could not retrieve the application-level substitution variable. " +
            "Substitution Variable: [" + subVarName + "]. " +
            "Error: " + e.getMessage()
        )
    }

    subVarValue = stripOuterQuotes(subVarValue)

    if (subVarValue.length() == 0 || subVarValue.equalsIgnoreCase("null")) {
        throwVetoException(
            "The substitution variable is empty or does not exist. " +
            "Substitution Variable: [" + subVarName + "]."
        )
    }

    return subVarValue
}


String mapSelectorValue(Map selectorMap, String inputValue, String label) {
    String input = inputValue == null ? "" : inputValue.trim()

    if (input.length() == 0) {
        throwVetoException("Empty selector input value: " + label)
    }

    if (selectorMap == null || selectorMap.isEmpty()) {
        return input
    }

    Object mapped = selectorMap.get(input)

    if (mapped == null) {
        for (Object entryObj : selectorMap.entrySet()) {
            Map.Entry entry = (Map.Entry) entryObj

            if (entry.getKey() != null &&
                entry.getKey().toString().equalsIgnoreCase(input)) {
                mapped = entry.getValue()
                break
            }
        }
    }

    if (mapped == null) {
        println(
            "WARNING: No selector mapping was found for " +
            label + " = [" + input + "]. The original value will be used."
        )
        return input
    }

    String output = mapped.toString().trim()

    if (output.length() == 0) {
        throwVetoException(
            "The selector mapping returned an empty value. " +
            "Selector: " + label + ", Input: [" + input + "]."
        )
    }

    println(
        "Selector mapping: " + label +
        " | input=[" + input + "] -> reportValue=[" + output + "]"
    )

    return output
}


String extractJobIdFromResponse(Map responseMap) {
    Object linksObj = responseMap.get("links")

    if (linksObj instanceof List) {
        List linksList = (List) linksObj

        for (Object linkObj : linksList) {
            if (!(linkObj instanceof Map)) {
                continue
            }

            Map linkMap = (Map) linkObj
            String rel = getMapText(linkMap, "rel")
            String href = getMapText(linkMap, "href")

            if (href != null && href.trim().length() > 0) {
                if (rel.equalsIgnoreCase("Job Status") ||
                    href.indexOf("/jobs/") >= 0) {
                    int lastSlash = href.lastIndexOf("/")

                    if (lastSlash >= 0 && lastSlash < href.length() - 1) {
                        return href.substring(lastSlash + 1)
                    }
                }
            }
        }
    }

    String jobId = getMapText(responseMap, "jobId")

    if (jobId != null && jobId.trim().length() > 0) {
        return jobId.trim()
    }

    return ""
}


String normalizeReportOutputPath(
    String outputFileName,
    String jobId,
    String reportFormat
) {
    String extension = reportFormat == null ? "xlsx" : reportFormat.toLowerCase()
    String outputPath = outputFileName == null
        ? ""
        : outputFileName.trim().replace("\\", "/")

    if (outputPath.length() == 0 || outputPath.equalsIgnoreCase("null")) {
        return "outbox/reports/" + jobId + "." + extension
    }

    if (outputPath.equalsIgnoreCase("outbox/reports") ||
        outputPath.equalsIgnoreCase("outbox/report")) {
        return outputPath + "/" + jobId + "." + extension
    }

    if (outputPath.endsWith("/")) {
        return outputPath + jobId + "." + extension
    }

    if (!outputPath.toLowerCase().startsWith("outbox/") &&
        !outputPath.toLowerCase().startsWith("inbox/") &&
        outputPath.indexOf("/") < 0) {
        return "outbox/reports/" + outputPath
    }

    return outputPath
}


String getUserEmailByLogin(String connectionName, String userLogin) {
    String requestedLogin = userLogin == null ? "" : userLogin.trim()

    if (requestedLogin.length() == 0) {
        throwVetoException("The rule executor user login is empty.")
    }

    Map userPayload = new LinkedHashMap()
    userPayload.put("userlogin", requestedLogin)
    userPayload.put("epmgroups", false)
    userPayload.put("granularroles", false)
    userPayload.put("idcsgroups", false)
    userPayload.put("applicationroles", false)
    userPayload.put("indirect", false)

    println("")
    println("====================================================")
    println("Resolving rule executor email address")
    println("Rule Executor Login : " + requestedLogin)
    println("====================================================")

    def userResponse = operation.application
        .getConnection(connectionName)
        .post("/interop/rest/security/v1/users/list")
        .header("Content-Type", "application/json")
        .header("Accept", "application/json")
        .body(json(userPayload))
        .asString()

    int httpStatus = userResponse.status as int
    String body = userResponse.body == null ? "" : userResponse.body.toString()

    println("List Users HTTP status: " + httpStatus)

    if (httpStatus < 200 || httpStatus > 299) {
        throwVetoException(
            "The Access Control List Users request failed. " +
            "User Login: [" + requestedLogin + "]. " +
            "HTTP status: " + httpStatus + ". " +
            "Response: " + body
        )
    }

    Map userMap = parseJsonToMap(body)
    String statusText = getMapText(userMap, "status")

    if (statusText != null && statusText.trim().length() > 0) {
        try {
            int apiStatus = statusText.toInteger()

            if (apiStatus != 0) {
                throwVetoException(
                    "The Access Control List Users API returned an error. " +
                    "Status: " + apiStatus + ". " +
                    "User Login: [" + requestedLogin + "]. " +
                    "Response: " + body
                )
            }
        } catch (NumberFormatException ignored) {
            println("WARNING: List Users status is not numeric: " + statusText)
        }
    }

    List usersList = getListFromPossibleKeys(
        userMap,
        ["details", "items", "users"]
    )

    if (usersList == null || usersList.isEmpty()) {
        if (requestedLogin.contains("@")) {
            println(
                "WARNING: Access Control did not return a user record. " +
                "The login is in email format and will be used as the recipient."
            )
            return requestedLogin
        }

        throwVetoException(
            "Access Control did not find the rule executor user. " +
            "User Login: [" + requestedLogin + "]. " +
            "Response: " + body
        )
    }

    Map selectedUser = null

    for (Object rowObj : usersList) {
        if (!(rowObj instanceof Map)) {
            continue
        }

        Map row = (Map) rowObj
        String returnedLogin = getMapText(row, "userlogin")

        if (returnedLogin == null || returnedLogin.trim().length() == 0) {
            returnedLogin = getMapText(row, "login")
        }

        if (returnedLogin == null || returnedLogin.trim().length() == 0) {
            returnedLogin = getMapText(row, "userName")
        }

        if (returnedLogin != null &&
            returnedLogin.equalsIgnoreCase(requestedLogin)) {
            selectedUser = row
            break
        }
    }

    if (selectedUser == null &&
        usersList.size() == 1 &&
        usersList.get(0) instanceof Map) {
        selectedUser = (Map) usersList.get(0)
    }

    if (selectedUser == null) {
        throwVetoException(
            "Access Control returned multiple users but no exact login match was found. " +
            "Requested Login: [" + requestedLogin + "]. " +
            "Response: " + body
        )
    }

    String email = getMapText(selectedUser, "email")

    if (email == null || email.trim().length() == 0) {
        email = getMapText(selectedUser, "emailAddress")
    }

    if (email == null || email.trim().length() == 0) {
        email = getMapText(selectedUser, "mail")
    }

    email = email == null ? "" : email.trim()

    if (email.length() == 0 || email.equalsIgnoreCase("null")) {
        if (requestedLogin.contains("@")) {
            println(
                "WARNING: The Access Control email field is empty. " +
                "The login is in email format and will be used as the recipient."
            )
            email = requestedLogin
        } else {
            throwVetoException(
                "No email address is configured for the rule executor. " +
                "User Login: [" + requestedLogin + "]."
            )
        }
    }

    if (!email.contains("@") ||
        email.startsWith("@") ||
        email.endsWith("@")) {
        throwVetoException(
            "Access Control returned an invalid email address for the rule executor. " +
            "User Login: [" + requestedLogin + "], Email: [" + email + "]."
        )
    }

    println("Resolved Rule Executor Login : " + requestedLogin)
    println("Resolved Recipient Email     : " + email)

    return email
}


Map getReportJobStatus(String connectionName, String jobId) {
    def statusResponse = operation.application
        .getConnection(connectionName)
        .get("/aif/rest/V1/jobs/" + jobId)
        .asString()

    int httpStatus = statusResponse.status as int
    String body = statusResponse.body == null ? "" : statusResponse.body.toString()

    if (httpStatus < 200 || httpStatus > 299) {
        throwVetoException(
            "The Data Management report status request failed. " +
            "Job ID: " + jobId + ". " +
            "HTTP status: " + httpStatus + ". " +
            "Response: " + body
        )
    }

    return parseJsonToMap(body)
}


Map getMailJobStatus(String connectionName, String jobId) {
    def statusResponse = operation.application
        .getConnection(connectionName)
        .get("/interop/rest/v2/status/jobs/" + jobId)
        .asString()

    int httpStatus = statusResponse.status as int
    String body = statusResponse.body == null ? "" : statusResponse.body.toString()

    if (httpStatus < 200 || httpStatus > 299) {
        throwVetoException(
            "The Send Mail status request failed. " +
            "Job ID: " + jobId + ". " +
            "HTTP status: " + httpStatus + ". " +
            "Response: " + body
        )
    }

    return parseJsonToMap(body)
}

/* =========================================================
   STEP 3 - VALIDATE CONNECTION AND EXECUTOR
   ========================================================= */

println("====================================================")
println("Starting FCCS Groovy Report Email Rule")
println("Connection       : " + CONNECTION_NAME)
println("Period SubVar    : " + PERIOD_SUBVAR_NAME)
println("Report           : " + REPORT_NAME)
println("====================================================")

if (!operation.application.hasConnection(CONNECTION_NAME)) {
    throwVetoException("Connection not found: " + CONNECTION_NAME)
}

User executorUser = operation.getUser()

if (executorUser == null) {
    throwVetoException("Could not determine the user who executed the rule.")
}

String executorLogin = executorUser.getName()

if (executorLogin == null || executorLogin.trim().length() == 0) {
    throwVetoException("The rule executor user ID is empty.")
}

executorLogin = executorLogin.trim()

String sSendTo = getUserEmailByLogin(
    CONNECTION_NAME,
    executorLogin
)

/* =========================================================
   STEP 4 - READ INPUT VALUES
   ========================================================= */

String inputPeriod = getRequiredApplicationSubVarValue(PERIOD_SUBVAR_NAME)

String inputCategory = requireText(
    rtps.vDM_Category == null ? "" : rtps.vDM_Category.toString(),
    "vDM_Category"
)

String inputLocation = requireText(
    rtps.vDM_Location == null ? "" : rtps.vDM_Location.toString(),
    "vDM_Location"
)

String inputIntegrationName = requireText(
    rtps.vDM_RuleName == null ? "" : rtps.vDM_RuleName.toString(),
    "vDM_RuleName"
)

/* =========================================================
   STEP 5 - APPLY SELECTOR MAPPING
   ========================================================= */

String sPeriod = mapSelectorValue(
    periodSelectorMap,
    inputPeriod,
    "Period"
)

String sCategory = mapSelectorValue(
    categorySelectorMap,
    inputCategory,
    "Category"
)

String sLocation = mapSelectorValue(
    locationSelectorMap,
    inputLocation,
    "Location"
)

String sIntegrationName = mapSelectorValue(
    integrationNameSelectorMap,
    inputIntegrationName,
    "Integration Name"
)

/* =========================================================
   STEP 6 - BUILD REPORT REQUEST
   ========================================================= */

Map reportParameters = new LinkedHashMap()
reportParameters.put("Period", sPeriod)
reportParameters.put("Category", sCategory)
reportParameters.put("Location", sLocation)
reportParameters.put("Integration Name", sIntegrationName)

Map reportPayload = new LinkedHashMap()
reportPayload.put("jobType", "REPORT")
reportPayload.put("jobName", REPORT_NAME)
reportPayload.put("reportFormatType", DEFAULT_REPORT_FORMAT)
reportPayload.put("parameters", reportParameters)

/* =========================================================
   STEP 7 - SUBMIT REPORT
   ========================================================= */

println("")
println("====================================================")
println("Starting Data Management Report Execution")
println("Execution Method : REST API")
println("Connection       : " + CONNECTION_NAME)
println("Rule Executor    : " + executorLogin)
println("Recipient        : " + sSendTo)
println("Report           : " + REPORT_NAME)
println("Format           : " + DEFAULT_REPORT_FORMAT)
println("Period Source    : Substitution Variable [" + PERIOD_SUBVAR_NAME + "]")
println("Period Value     : " + sPeriod)
println("====================================================")

println("")
println("Final report parameter values:")

for (Object keyObj : reportParameters.keySet()) {
    String key = keyObj.toString()
    println(key + " = [" + reportParameters.get(key) + "]")
}

println("")
println("Report request payload:")
println(json(reportPayload))

def submitResponse = operation.application
    .getConnection(CONNECTION_NAME)
    .post("/aif/rest/V1/jobs")
    .header("Content-Type", "application/json")
    .header("Accept", "application/json")
    .body(json(reportPayload))
    .asString()

int submitHttpStatus = submitResponse.status as int
String submitBody = submitResponse.body == null ? "" : submitResponse.body.toString()

println("")
println("Submit report HTTP status: " + submitHttpStatus)
println("Submit report response:")
println(submitBody)

if (submitHttpStatus < 200 || submitHttpStatus > 299) {
    throwVetoException(
        "The Data Management report request failed. " +
        "Report: " + REPORT_NAME + ". " +
        "HTTP status: " + submitHttpStatus + ". " +
        "Response: " + submitBody
    )
}

Map submitMap = parseJsonToMap(submitBody)
String reportJobId = getMapText(submitMap, "jobId")

if (reportJobId == null || reportJobId.trim().length() == 0) {
    reportJobId = extractJobIdFromResponse(submitMap)
}

reportJobId = reportJobId == null ? "" : reportJobId.trim()

if (reportJobId.length() == 0) {
    throwVetoException(
        "The report request returned a successful HTTP response but no job ID. " +
        "Response: " + submitBody
    )
}

println("")
println("Report submitted.")
println("Report Job ID: " + reportJobId)

/* =========================================================
   STEP 8 - WAIT FOR REPORT COMPLETION
   ========================================================= */

int finalStatus = getStatusFromMap(submitMap)
String finalJobStatusText = getMapText(submitMap, "jobStatus")
String finalLogFileName = getMapText(submitMap, "logFileName")
String finalOutputFileName = getMapText(submitMap, "outputFileName")
String finalDetails = getMapText(submitMap, "details")
String finalRawBody = submitBody

if (finalStatus == 999) {
    finalStatus = -1
}

int pollCount = 0
long delay = INITIAL_POLL_DELAY_MS

while (finalStatus == -1 && pollCount < REPORT_MAX_POLL_COUNT) {
    sleep(delay)

    pollCount = pollCount + 1
    delay = delay * 2L

    if (delay > MAX_POLL_DELAY_MS) {
        delay = MAX_POLL_DELAY_MS
    }

    Map statusMap = getReportJobStatus(CONNECTION_NAME, reportJobId)

    finalStatus = getStatusFromMap(statusMap)
    finalJobStatusText = getMapText(statusMap, "jobStatus")
    finalLogFileName = getMapText(statusMap, "logFileName")
    finalOutputFileName = getMapText(statusMap, "outputFileName")
    finalDetails = getMapText(statusMap, "details")
    finalRawBody = statusMap.toString()

    println(
        "Report polling " + pollCount +
        " | Job ID: " + reportJobId +
        " | status: " + finalStatus +
        " | jobStatus: " + finalJobStatusText +
        " | outputFileName: " + finalOutputFileName
    )
}

if (finalStatus == -1) {
    throwVetoException(
        "The report job did not complete within the polling limit. " +
        "Report: " + REPORT_NAME + ". " +
        "Job ID: " + reportJobId + ". " +
        "Last response: " + finalRawBody
    )
}

if (finalStatus != 0) {
    throwVetoException(
        "The Data Management report job failed. " +
        "Report: " + REPORT_NAME +
        ", Job ID: " + reportJobId +
        ", Status: " + finalStatus +
        ", Job Status: " + finalJobStatusText +
        ", Details: " + finalDetails +
        ", Raw Response: " + finalRawBody
    )
}

if ((finalOutputFileName == null || finalOutputFileName.trim().length() == 0) &&
    finalDetails != null &&
    finalDetails.toLowerCase().endsWith("." + DEFAULT_REPORT_FORMAT.toLowerCase())) {
    finalOutputFileName = finalDetails
}

String attachmentPath = normalizeReportOutputPath(
    finalOutputFileName,
    reportJobId,
    DEFAULT_REPORT_FORMAT
)

println("")
println("Report completed successfully.")
println("Report Job ID    : " + reportJobId)
println("Output File Name : " + finalOutputFileName)
println("Attachment Path  : " + attachmentPath)
println("Log File         : " + finalLogFileName)

/* =========================================================
   STEP 9 - BUILD EMAIL
   ========================================================= */

String desiredFileName =
    sanitizeFilePart(sIntegrationName) + "_" +
    sanitizeFilePart(sPeriod) + "_" +
    "TB_All_Columns." + DEFAULT_REPORT_FORMAT.toLowerCase()

String emailSubject =
    EMAIL_SUBJECT_PREFIX + " - " +
    sanitizeFilePart(sIntegrationName) + " - " +
    sanitizeFilePart(sPeriod)

String emailBody =
    "<html><body>" +
    "<p>Data Integration report completed successfully.</p>" +
    "<p><strong>Rule executor:</strong> " + executorLogin + "</p>" +
    "<p><strong>Recipient:</strong> " + sSendTo + "</p>" +
    "<p><strong>Report:</strong> " + REPORT_NAME + "</p>" +
    "<p><strong>Period source:</strong> Substitution Variable " + PERIOD_SUBVAR_NAME + "</p>" +
    "<p><strong>Period:</strong> " + sPeriod + "</p>" +
    "<p><strong>Category:</strong> " + sCategory + "</p>" +
    "<p><strong>Location:</strong> " + sLocation + "</p>" +
    "<p><strong>Integration Name:</strong> " + sIntegrationName + "</p>" +
    "<p><strong>Requested business file name:</strong> " + desiredFileName + "</p>" +
    "<p><strong>Attached EPM repository file:</strong> " + attachmentPath + "</p>" +
    "<p>Note: EPM generated the physical attachment name. The business file name is shown above.</p>" +
    "</body></html>"

println("")
println("Email subject       : " + emailSubject)
println("Requested file name : " + desiredFileName)
println("Actual attachment   : " + attachmentPath)
println("Recipient           : " + sSendTo)

/* =========================================================
   STEP 10 - SEND EMAIL
   ========================================================= */

Map mailParameters = new LinkedHashMap()
mailParameters.put("attachments", attachmentPath)

Map mailPayload = new LinkedHashMap()
mailPayload.put("to", sSendTo)
mailPayload.put("subject", emailSubject)
mailPayload.put("body", emailBody)
mailPayload.put("parameters", mailParameters)

println("")
println("Submitting Send Mail REST API v2.")
println("Recipient   : " + sSendTo)
println("Subject     : " + emailSubject)
println("Attachments : " + attachmentPath)

println("")
println("Send Mail payload:")
println(json(mailPayload))

def mailResponse = operation.application
    .getConnection(CONNECTION_NAME)
    .post("/interop/rest/v2/mails/send")
    .header("Content-Type", "application/json")
    .header("Accept", "application/json")
    .body(json(mailPayload))
    .asString()

int mailHttpStatus = mailResponse.status as int
String mailBody = mailResponse.body == null ? "" : mailResponse.body.toString()

println("")
println("Send Mail HTTP status: " + mailHttpStatus)
println("Send Mail response:")
println(mailBody)

if (mailHttpStatus < 200 || mailHttpStatus > 299) {
    throwVetoException(
        "The Send Mail REST API v2 request failed. " +
        "HTTP status: " + mailHttpStatus + ". " +
        "Response: " + mailBody
    )
}

Map mailMap = parseJsonToMap(mailBody)
int mailStatus = getStatusFromMap(mailMap)
String mailJobId = extractJobIdFromResponse(mailMap)

if (mailStatus == 999 &&
    mailJobId != null &&
    mailJobId.trim().length() > 0) {
    mailStatus = -1
}

println("Send Mail initial status: " + mailStatus)
println("Send Mail job ID        : " + mailJobId)

/* =========================================================
   STEP 11 - WAIT FOR EMAIL COMPLETION
   ========================================================= */

if (mailStatus == -1 &&
    mailJobId != null &&
    mailJobId.trim().length() > 0) {

    int mailPollCount = 0
    long mailDelay = INITIAL_POLL_DELAY_MS

    while (mailStatus == -1 && mailPollCount < MAIL_MAX_POLL_COUNT) {
        sleep(mailDelay)

        mailPollCount = mailPollCount + 1
        mailDelay = mailDelay * 2L

        if (mailDelay > MAX_POLL_DELAY_MS) {
            mailDelay = MAX_POLL_DELAY_MS
        }

        Map mailStatusMap = getMailJobStatus(CONNECTION_NAME, mailJobId)
        mailStatus = getStatusFromMap(mailStatusMap)

        println(
            "Send Mail polling " + mailPollCount +
            " | Job ID: " + mailJobId +
            " | status: " + mailStatus +
            " | details: " + getMapText(mailStatusMap, "details")
        )
    }
}

if (mailStatus == -1) {
    throwVetoException(
        "The email job did not complete within the polling limit. " +
        "Mail Job ID: " + mailJobId
    )
}

if (mailStatus != 0) {
    throwVetoException(
        "Email delivery failed. " +
        "Send Mail status: " + mailStatus + ". " +
        "Initial response: " + mailBody
    )
}

/* =========================================================
   STEP 12 - FINAL LOG
   ========================================================= */

println("")
println("====================================================")
println("Data Management report completed and email sent successfully.")
println("Rule Executor        : " + executorLogin)
println("Period SubVar        : " + PERIOD_SUBVAR_NAME)
println("Period Value         : " + sPeriod)
println("Report Name          : " + REPORT_NAME)
println("Report Job ID        : " + reportJobId)
println("Actual Attachment    : " + attachmentPath)
println("Requested File Name  : " + desiredFileName)
println("Sent To              : " + sSendTo)
println("====================================================")

Production checks

CheckExpected result
Named connection existsoperation.application.hasConnection("EPM_REST") returns true.
Executor is provisionedThe Access Control API returns the exact login and a valid email address.
DataPush is application-levelThe Groovy API can retrieve the value regardless of the cube context.
Report definition name is exactThe payload uses TB All Columns (Per,Cat,Loc), not the underlying query name.
RTP values are validCategory, Location, and Integration Name exist and return report data.
Mail permissions are availableThe Send Mail REST job completes with status 0.
EPMXpert Advisory • Oracle EPM Cloud automation pattern
Written by Bertold Bicsak

EPMXpert design and implement new Oracle EPM Cloud applications as part of our engagements, covering Planning, FCCS, and Narrative Reporting. Implementation is delivered within our service packages and includes environment setup, configuration, testing, and transition to steady-state support.

EPMXpert design and implement new Oracle EPM Cloud applications as part of our engagements, covering Planning, FCCS, and Narrative Reporting. Implementation is delivered within our service packages and includes environment setup, configuration, testing, and transition to steady-state support.