Conflict Checker
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.
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> 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:
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.