Skip to main content
Skip table of contents

Conflict Checker

The scheduling configuration has been improved as part of Golive 9.23. If you are using Golive 9.23 or a newer version, please refer to the Scheduling Environments section. If you are using an older version of Golive, consider upgrading. Alternatively, you can refer to the documentation below for configurating your environment scheduling.

When you book an environment, you need to make sure you are not booking it on the same timeslot than a colleague. In our basic use case, we rely on the Timeline to visualize potential conflicts. If you want to go further and highlight or block conflicting booking requests, read further…

Highlight Conflicts with ScriptRunner

If you are using the ScriptRunner Jira App, you can display potential conflicts on your booking screen:

1. Add a new Script

After installing the ScriptRunner Jira App, add our script in the Script Editor (code reference below).

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

The folder hierarchy (com/apwide/behaviour) should correspond to the import line you have in your script (point 3 below): import com.apwide.behaviour.ConflictChecker

You can ignore the warnings displayed in the script editor, about some classes (environment, ctx) unknown at this stage.

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()
    }


}

2. Add a new Behaviour

Then, 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 your Behaviour

In your Behaviour, create an Initialiser Script: and call the Conflict Checker script we added previously:

And 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: your Booking Request issue type name

  • startDateFieldName: your booking start date/time field name

  • endDateFieldName: your booking end date/time custom field name

  • environmentFieldName: your Environment custom field name

  • helpFieldName: the name of the field under which the warning message should appear (if not defined, it will be your endDateFieldName)

  • timelineUrl: URL used in the warning message “Open the Timeline” button

  • dateFormat: date format used for the period (if not defined, it will be your Jira date format)

  • blocking: do you need to block the request creation (true) or not (false) in case of detected conflict. When the blocking parameter is set to true, the warning banner will be displayed in red:

  • showDependencies: decide to show (true) or hide (false) the list of dependencies of booked environments (with blue background on the screenshot)

  • calendarId (optional): ID of a Scheduled Event Calendar to include in the conflict detection. Calendars' IDs (Integer) can be found in the “Shared Calendars” section (HTML source code). This parameter is optional and does not appear in the script examples below.

In order to check for conflicts each time the fields are updated in your Booking Request, add the 3 fields in your Behaviour:

  • Environment field

  • Start time field

  • End time field

Mark them as “Required” using the first toggle, and for each of them, add the same Server-side scripts used than for “Initialiser”:

Here an exmple for the “End time” field

All scripts contained by the same behaviour must be identical. We recommend you to store the code in a common script file shared by the Initialiser and the Fields instead of copying the code.

JavaScript errors detected

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

If this problem persists, please contact our support.