How hard can it be?
So, in our code we retrieve a custom field and we have an issue, and we just get the custom field value right?
final CustomField customField = customFieldManager.getCustomFieldObjectByName("My Multi-select");
final Object value = issue.getCustomFieldValue(customField);
Great, now we're done!
Object?! That's no use...
Hmmm, Object isn't going to be very helpful. Let's assume it's a List<String> because that makes sense for a multiselect right?
final CustomField customField = customFieldManager.getCustomFieldObjectByName("My Multi-select");
@SuppressWarnings("unchecked")
final List<String> value = (List<String>) issue.getCustomFieldValue(customField);
log.debug(value.get(0));
What could possibly go wrong?
NullPointerException
Ah yes, that. Wait... does Java even have pointers?!
So it turns out we forgot to assign a value to our custom field in JIRA. Oops.
Let's fix that "edge case" up quickly.
final CustomField customField = customFieldManager.getCustomFieldObjectByName("My Multi-select");
@SuppressWarnings("unchecked")
final List<String> value = (List<String>) issue.getCustomFieldValue(customField);
if (value != null) {
log.debug("No value assigned to custom field 'My Multi-select' on issue {}", issue.getKey());
} else {
log.debug(value.get(0));
}
Alright, sorted. Nice.
ClassCastException
java.lang.ClassCastException: com.atlassian.jira.issue.customfields.option.LazyLoadedOption cannot be
cast to java.lang.String
D'oh! Well, at least we know what's really returned by the Issue#getCustomFieldValue for a multi-select now!
OK, so we can just cast to the right kind of List now right?
NB: Annoyingly, Java won't throw a ClassCastException here if the returned List is of a different type the type its being cast to.
final CustomField customField = customFieldManager.getCustomFieldObjectByName("My Multi-select");
@SuppressWarnings("unchecked")
final List<LazyLoadedOption> value = (List<LazyLoadedOption>) issue.getCustomFieldValue(customField);
if (value != null) {
log.debug("No value assigned to custom field 'My Multi-select' on issue {}", issue.getKey());
} else {
final LazyLoadedOption opt = value.get(0);
log.debug(opt.getValue());
}
Great, let's tidy this up into a useful method!
Refactoring
/**
* Useful method for retrieving multi-select custom field value as List<String>
*/
@Nonnull
public List<String> getMultiSelectFieldValue(@Nonnull String fieldName, @Nonnull Issue issue) {
Validate.notNull(fieldName);
Validate.notNull(issue);
final CustomField customField = customFieldManager.getCustomFieldObjectByName(fieldName);
// Let's use the Option interface here as well instead of a specific implementation
@SuppressWarnings("unchecked")
final List<Option> value = (List<Option>) issue.getCustomFieldValue(customField);
if (value == null) {
log.debug("No value assigned to custom field 'My Multi-select' on issue {}", issue.getKey());
return Lists.newArrayList();
}
// Oooo, Java 8! Shiny!
return value.stream()
.map(Option::getValue)
.collect(Collectors.toList());
}
Now we're clearly finished right?
Do you trust your JIRA Admin?
Don't.
Remember, rule 101 about external input to computer programs?
You can't trust anyone!
How is our shiny new method going to cope if the custom field we've told it to use isn't a multi-select?
java.lang.ClassCastException: java.lang.String cannot be cast to java.util.List
Our good friend ClassCastException again!
Let's get defensive
Let's check we actually have what we're expecting!
/**
* Useful method for retrieving multi-select custom field value as List<String>
*/
@Nonnull
public List<String> getMultiSelectFieldValue(@Nonnull String fieldName, @Nonnull Issue issue) {
Validate.notNull(fieldName);
Validate.notNull(issue);
final CustomField customField = customFieldManager.getCustomFieldObjectByName(fieldName);
// Let's use the Option interface here as well instead of a specific implementation
@SuppressWarnings("unchecked")
final List<Option> value = (List<Option>) issue.getCustomFieldValue(customField);
// Handle NullPointerException
if (value == null) {
log.debug(
"No value assigned to custom field '{}' on issue {}. Returning empty list.",
customField, issue.getKey()
);
return Lists.newArrayList();
}
// Handle non-list return values
if (!(value instanceof List)) {
log.debug(
"Value of custom field '{}' on issue {} was not a List. Returning empty list.",
customField, issue.getKey()
);
return Lists.newArrayList();
}
// If it's empty, lets just return a new empty string list and forget about the origin type
if (value.isEmpty()) {
return Lists.newArrayList();
}
// Handle potential ClassCastException for lists of any other kind, like Label
if (!(value.get(0) instanceof Option)) {
log.debug(
"Value of custom field '{}' on issue {} was not a List<Option>. Returning empty list.",
customField, issue.getKey()
);
return Lists.newArrayList();
}
// Oooo, Java 8! Shiny!
return value.stream()
.map(Option::getValue)
.collect(Collectors.toList());
}
Overkill?
That depends on how you want failures to be treated within your code.
The code above essentially fails silently, trying to recover from all possible scenarios by returning an empty data set. This may work perfectly in some scenarios, but be a disaster in others.
As the developer you need to work with the customer to determine how each of these edge cases should be handled. Most of the additional code in the example above handles exceptional circumstances, so perhaps throwing an Exception would be the correct thing to do - it just often feels like a nasty user experience if the UI breaks and displays a huge stack trace.
At least with the code above the behaviour degrades gracefully and if a user isn't seeing what they expect they can flag that up with someone who can probably ask someone else to look at the logs and find out what's going wrong.
At the very least as developers we should be asking the questions that led us to this result, rather than only coding the happy path.
- 09 Oct 2018 » A strange bug on AWS Lambda
- 17 Jan 2018 » How to run Karma tests in browsers in Docker
- 07 Dec 2017 » Switching from Javascript to Typescript
- 30 Oct 2017 » Fun with React event handlers
- 17 Jul 2017 » Switching from Groovy to Java
- 24 May 2017 » Useful Git Aliases
- 27 Mar 2017 » Practical Ratpack Promises
- 03 Nov 2016 » Custom Content in Forms for Confluence Connect
- 04 Oct 2016 » Checking user permissions from REST calls
- 30 Sep 2016 » Using the reflection API in Confluence
- 28 Sep 2016 » Creating a custom Confluence Blueprint
- 06 Sep 2016 » ReactJS in Forms for Confluence Connect
- 25 Apr 2016 » Migrating to ES6 in Atlassian Add-ons
- 17 Mar 2016 » All kinds of things I learnt trying to performance test against Fisheye/Crucible
- 24 Dec 2015 » Adaptavist’s Holiday Gift of Atlassian Deployment Automation
- 17 Dec 2015 » Getting a Custom Field value safely
- 07 Dec 2015 » Putting Google Analytics to work with plugins for Confluence
- 02 Dec 2015 » Devoxx Voting, A retrospective
- 25 Nov 2015 » Some things I've learnt about SingleSelect
- 15 Oct 2015 » Using SOY for JIRA actions
- 26 Sep 2015 » Object Reflection in Groovy
- 22 Sep 2015 » Introducing Adaptavist Labs