Skip to main content
Skip table of contents

Behaviours - Conflict Checker

When you book an Environment, you need ensure that you are not booking it on the same timeslot than a colleague. With ScriptRunner Behaviour, you can display a warning banner on your booking creation screen, in case a conflict is detected. This guide explains how to configure a Conflict Checker banner on your booking creation screen in Jira Data Center.

Example of Conflict Checker banner displayed with ScriptRunner Behaviour


Requirements


Configuration Steps

Step 1: Add a New Script

Add our script in the ScriptRunner Script Editor (code reference below).

New Script in the Script Editor section (check below for the code)

Important

Ensure the folder hierarchy (com/apwide/behaviour) matches the import line in the script.

Info

Ignore any warnings in the script editor related to unknown classes (environment, ctx).

GROOVY
package com.apwide.behaviour

import com.apwide.env.api.Environment
import com.apwide.env.api.Golive
import com.apwide.env.api.ScheduledEvent
import com.apwide.env.api.ScheduledEvents
import com.atlassian.jira.bc.issue.search.SearchService
import com.atlassian.jira.component.ComponentAccessor
import com.atlassian.jira.entity.WithKey
import com.atlassian.jira.issue.Issue
import com.atlassian.jira.issue.context.IssueContext
import com.atlassian.jira.issue.fields.CustomField
import com.atlassian.jira.user.ApplicationUser
import com.atlassian.query.Query
import com.onresolve.jira.groovy.user.FormField
import com.onresolve.scriptrunner.runner.customisers.WithPlugin
import com.opensymphony.workflow.InvalidInputException
import groovy.util.logging.Log4j
import org.apache.log4j.Level
import org.codehaus.groovy.runtime.typehandling.GroovyCastException

import java.util.stream.Collectors

import static com.atlassian.jira.config.properties.APKeys.JIRA_DATE_TIME_PICKER_JAVA_FORMAT
import static com.atlassian.jira.web.bean.PagerFilter.getUnlimitedFilter

@WithPlugin("com.holydev.env.plugin.jira-holydev-env-plugin")
@Log4j
class ConflictChecker {

    static final String JIRA_JQL_DATE_FORMAT = 'yyyy-MM-dd HH:mm'

    def ctx
    Golive golive
    String dateFormat
    String issueTypeName
    String startDateFieldName
    String endDateFieldName
    String environmentFieldName
    String timelineUrl
    String helpFieldName
    boolean blocking = false
    boolean showDependencies = true
    Integer calendarId
    String validationErrorMessage
    Level logLevel

    void validate() {
        String message = evaluate()
        if (!message.isEmpty()) {
            log.debug("Validator rejects")
            throw new InvalidInputException(validationErrorMessage)
        }
    }

    void check() {
        writeHelpText(evaluate())
    }

    String evaluate() {
        log.setLevel(logLevel ?: Level.INFO)

        Date startFieldValue = getDateValue(startDateFieldName)
        Date endFieldValue = getDateValue(endDateFieldName)
        List bookedEnvironmentIds = getEnvironmentsValue(environmentFieldName)

        if (!startFieldValue || !endFieldValue || !bookedEnvironmentIds) {
            log.debug("No values found")
            return ""
        }

        List<Environment> dependencies = showDependencies ? getDependencies(bookedEnvironmentIds) : []
        List<Issue> conflicts = searchForConflicts(startFieldValue, endFieldValue, bookedEnvironmentIds)
        List<ScheduledEvent> events = searchForEvents(startFieldValue, endFieldValue, bookedEnvironmentIds, calendarId)

        if (conflicts.isEmpty() && events.isEmpty() && dependencies.isEmpty()) {
            log.debug("Nothing to display")
            return ""
        }

        log.debug("Found ${dependencies.size()} depencencies")
        log.debug("Found ${conflicts.size()} conflicts")
        log.debug("Found ${events.size()} events")

        def dependenciesInformation = ""
        def conflictsWarning = ""
        def eventsWarning = ""

        if (!dependencies.isEmpty()) {
            dependenciesInformation =
                    """
         <div class="aui-message aui-message-info">
            <p style="padding: 0">Unbooked Dependencies</p>
            <table style="margin: 15px 0px; width:100%">
              <tr>
                <th>Name</th>
                <th>Category</th>
                <th>Application</th>
              </tr>
              ${renderEnvironmentList(dependencies)}
            </table>
         </div>
        """
        }

        if (!conflicts.isEmpty()) {
            conflictsWarning = """
        <p style="padding: 0">There are conflicting Booking Requests:</p>
        <table style="margin: 15px 0px; width:100%">
          <tr>
            <th>Key</th>
            <th>Summary</th>
            <th>Period</th>
            <th>Environment(s)</th>
          </tr>
          ${renderConflictList(conflicts, bookedEnvironmentIds)}
        </table>
      """
        }

        if (!events.isEmpty()) {
            eventsWarning = """
        <p style="padding: 0">There are conflicting Events:</p>
        <table style="margin: 15px 0px; width:100%">
          <tr>
            <th>Id</th>
            <th>Summary</th>
            <th>Period</th>
            <th>Environment(s)</th>
          </tr>
          ${renderEventConflictList(events, bookedEnvironmentIds)}
        </table>
      """
        }

        def conflictsInformation = ""

        if (!conflicts.isEmpty() || !events.isEmpty()) {
            conflictsInformation = """
          <div class="aui-message aui-message-${blocking ? 'error' : 'warning'}">
            ${conflictsWarning}
            ${eventsWarning}
            <a target="_blank" href="${timelineUrl}" class="aui-button">
              <span class="aui-icon aui-icon-small aui-iconfont-search"></span>&nbsp;Open the Timeline
            </a>
          </div>
        """
        }

        return """
      ${dependenciesInformation}
      ${conflictsInformation}
    """
    }

    private String renderEnvironmentList(List environments) {
        return environments.collect { environment ->
            return """
        <tr>
          <td><a href="/secure/ApwideGolive.jspa#/home/environment/${environment.id}" target="_blank">${environment.name}</a></td>
          <td>${environment.category.name ?: 'None'}</td>
          <td>${environment.application.name ?: 'None'}</td>
        </tr>
      """
        }.join("\n")
    }

    private String renderEventConflictList(List<ScheduledEvent> conflicts, Collection<String> environmentFieldValue) {
        return conflicts.collect { event ->
            Date startDate = event.getStart()
            Date endDate = event.getEnd()
            List environments = (List) event.getPlannedEnvironments().findAll { environment -> (environmentFieldValue?.find { (it + "").equals(environment.id + "") }) }

            return """
        <tr>
          <td>${event.id}</td>
          <td>${event.title}</td>
          <td>${displayedDate(startDate)} - ${displayedDate(endDate)}</td>
          <td>${displayedEnvironments(environments)}</td>
        </tr>
      """
        }.join("\n")
    }

    private String renderConflictList(List<Issue> conflicts, Collection<String> environmentFieldValue) {
        CustomField startCustomField = getCustomFieldByName(startDateFieldName)
        CustomField endCustomField = getCustomFieldByName(endDateFieldName)
        CustomField environmentCustomField = getCustomFieldByName(environmentFieldName)

        return conflicts.collect { issue ->
            Date startDate = (Date) issue.getCustomFieldValue(startCustomField)
            Date endDate = (Date) issue.getCustomFieldValue(endCustomField)
            Collection environments = issue.getCustomFieldValue(environmentCustomField)?.findAll { environment -> (environmentFieldValue?.find { (it + "").equals(environment.id + "") }) }

            return """
        <tr>
          <td><a target="_blank" href="${baseUrl()}/browse/${issue.key}">${issue.key}</a></td>
          <td>${issue.summary}</td>
          <td>${displayedDate(startDate)} - ${displayedDate(endDate)}</td>
          <td>${displayedEnvironments(environments)}</td>
        </tr>
      """
        }.join("\n")
    }

    private void writeHelpText(String helpText) {
        FormField endField = ctx.getFieldByName(helpFieldName ?: environmentFieldName)
        if (blocking) {
            endField.setError(helpText)
        } else {
            endField.setHelpText(helpText)
        }
    }

    private List<Environment> getDependencies(List<String> envIds) {
        if (!envIds || envIds.isEmpty()) {
            return []
        }
        Collection<Environment> dependencies = envIds.stream().flatMap {
            Environment env = golive.environments.getById(Integer.valueOf(it))
            return env.outgoingDependencies.stream()
        }
                .filter { !envIds.contains(it.id.toString()) }
                .collect(Collectors.toList())
        return dependencies.unique { a, b -> a.id <=> b.id }
    }

    private List<Issue> searchForConflicts(Date startFieldValue, Date endFieldValue, List environmentFieldValue) {
        String jql = toQuery(startFieldValue, endFieldValue, environmentFieldValue)
        Query query = searchService().parseQuery(loggedInUser(), jql).query
        return searchService().searchOverrideSecurity(loggedInUser(), query, getUnlimitedFilter()).results
    }

    private String toQuery(Date startFieldValue, Date endFieldValue, List environmentFieldValue) {
        String stringQuery = """
      type = "${issueTypeName}"
      AND "${startDateFieldName}" < "${jqlDate(endFieldValue)}"
      AND "${endDateFieldName}" > "${jqlDate(startFieldValue)}"
      AND "${environmentFieldName}" in (${environmentFieldValue.join(",")})
      ${notCurrentIssue()}
    """
        log.debug("query is: ${stringQuery}")
        return stringQuery
    }

    private String notCurrentIssue() {
        IssueContext issueContext = ctx.getIssueContext()
        if (issueContext instanceof WithKey) {
            return "AND key != \"${((WithKey) issueContext).getKey()}\""
        } else {
            return ""
        }
    }

    private String jqlDate(Date date) {
        return date.format(JIRA_JQL_DATE_FORMAT)
    }


    private List<ScheduledEvent> searchForEvents(Date startFieldValue, Date endFieldValue, List environmentFieldValue, Integer calendarId) {
        if (calendarId == null || environmentFieldValue == null) {
            return Collections.emptyList()
        }

        return golive.scheduledEvents.find(ScheduledEvents.SearchCriteria.builder()
                .calendarId(calendarId)
                .startBefore(endFieldValue)
                .endAfter(startFieldValue)
                .plannedEnvironmentIds(environmentFieldValue.collect { Integer.valueOf(it) })
                .build()
        )
    }

    private Date getDateValue(String fieldName) {
        try {
            FormField dateField = ctx.getFieldByName(fieldName)
            return dateField ? (Date) dateField.getValue() : null
        } catch (GroovyCastException ex) {
            return null
        }
    }

    private List getEnvironmentsValue(String fieldName) {
        try {
            FormField environmentField = ctx.getFieldByName(fieldName)
            return environmentField ? (List) environmentField.getValue() : null
        } catch (GroovyCastException ex) {
            return null
        }
    }

    private String displayedDate(Date date) {
        return date.format(displayDateFormat())
    }

    private String displayedEnvironments(Collection environments) {
        return environments ? environments.collect { it.name }.join("<br />") : "-"
    }

    private String displayedPlannedEnvironments(Collection plannedEnvironments) {
        return displayedEnvironments(plannedEnvironments.collect { golive.environments.getById(it.getEnvironmentId()) })
    }

    private String displayDateFormat() {
        def format = dateFormat != null ? dateFormat : ComponentAccessor.getApplicationProperties().getString(JIRA_DATE_TIME_PICKER_JAVA_FORMAT)
        log.debug("date format to be used will '${format}'")
        return format
    }

    private String baseUrl() {
        return ComponentAccessor.getApplicationProperties().getString("jira.baseurl")
    }

    private CustomField getCustomFieldByName(String name) {
        return ComponentAccessor.getCustomFieldManager().getCustomFieldObjectByName(name)
    }

    private SearchService searchService() {
        return ComponentAccessor.getComponent(SearchService.class)
    }

    private ApplicationUser loggedInUser() {
        return ComponentAccessor.getJiraAuthenticationContext().getLoggedInUser()
    }


}

Step 2: Add a New Behaviour

Create a new Behaviour and map it with your Booking Request project and issue type.

New entry in the Behaviour section

3. Call the Script from our Behaviour

  1. In your Behaviour, create an Initialiser Script.

2. Copy-paste the following code in the opened text area to call our Conflict Checker script:

CODE
package com.apwide.behaviour

import com.apwide.behaviour.ConflictChecker
import com.onresolve.scriptrunner.runner.customisers.WithPlugin
import com.onresolve.scriptrunner.runner.customisers.PluginModule
import com.apwide.env.api.GoliveAccessor
import org.apache.log4j.Level

@WithPlugin("com.holydev.env.plugin.jira-holydev-env-plugin")
@PluginModule
GoliveAccessor goliveAccessor

new ConflictChecker(
    ctx: this,
    golive: goliveAccessor.golive(),
    issueTypeName: "Booking Request",
    startDateFieldName: "Start time",
    endDateFieldName: "End time",
    environmentFieldName: "Environment(s) to book",
    helpFieldName: "Environment(s) to book",
    timelineUrl: "/secure/ApwideGolive.jspa#/home/timeline?view=timeline318",
    dateFormat: "MMM d",
    blocking: false,
    showDependencies: true,
    logLevel: Level.DEBUG
).check()

Script Parameters:

  • issueTypeName: Name of your Booking Request issue type.

  • startDateFieldName: Name of your booking start date/time field.

  • endDateFieldName: Name of your booking end date/time field.

  • environmentFieldName: Name of your Environment custom field.

  • helpFieldName: Field name for displaying warning messages. Defaults to endDateFieldName if undefined.

  • timelineUrl: URL for the “Open the Timeline” button in warning messages.

  • dateFormat: Format for the displayed period. Defaults to Jira's date format if undefined.

  • blocking: Set to true to block request creation when conflicts are detected (red warning). Set to false for non-blocking behavior (yellow warning):

  • showDependencies: Set to true to display a list of booked dependencies (blue background). Set to false to hide them.

  • calendarId (optional): ID of a Scheduled Event Calendar to include in the conflict detection. Retrieve this ID from the “Shared Calendars” section in the HTML source code.


Checking Conflicts in your Booking Request

To check for conflicts each time the fields are updated in your Booking Request:,

1. Add the following fields to your Behavior:

  • Environment field

  • Start time field

  • End time field

  1. Mark these fields as Required using the toggle.

  2. Add the same Server-side script for these fields as you use for the Initialiser.

Example for the “End time” field

Info

Ensure all scripts within the same Behavior are identical. We recommend using a shared script file for the Initialiser and Fields to maintain consistency and avoid copying code.


Need Assistance?

For support with integrating Golive and Jira Automation, reach out to our help center.

JavaScript errors detected

Please note, these errors can depend on your browser setup.

If this problem persists, please contact our support.