PageRenderTime 88ms CodeModel.GetById 36ms RepoModel.GetById 1ms app.codeStats 0ms

/atlassian-kinect-wallboard-extensions/src/main/java/com/atlassian/jirawallboard/resttransitionproxy/TransitionAsUserResource.java

https://bitbucket.org/shamid/kinect-wallboards/
Java | 563 lines | 472 code | 64 blank | 27 comment | 58 complexity | bb3a0b71ccafd5bbd0e3435fa26de2d9 MD5 | raw file
Possible License(s): GPL-3.0, LGPL-3.0, BSD-3-Clause
  1. package com.atlassian.jirawallboard.resttransitionproxy;
  2. import com.atlassian.crowd.embedded.api.User;
  3. import com.atlassian.jira.JiraDataTypes;
  4. import com.atlassian.jira.bc.issue.IssueService;
  5. import com.atlassian.jira.issue.Issue;
  6. import com.atlassian.jira.issue.IssueFieldConstants;
  7. import com.atlassian.jira.issue.IssueInputParameters;
  8. import com.atlassian.jira.issue.IssueInputParametersImpl;
  9. import com.atlassian.jira.issue.IssueManager;
  10. import com.atlassian.jira.issue.MutableIssue;
  11. import com.atlassian.jira.issue.fields.CustomField;
  12. import com.atlassian.jira.issue.fields.FieldManager;
  13. import com.atlassian.jira.issue.fields.OrderableField;
  14. import com.atlassian.jira.issue.fields.screen.FieldScreenRenderLayoutItem;
  15. import com.atlassian.jira.issue.fields.screen.FieldScreenRenderLayoutItemImpl;
  16. import com.atlassian.jira.issue.fields.screen.FieldScreenRenderTab;
  17. import com.atlassian.jira.issue.fields.screen.FieldScreenRenderer;
  18. import com.atlassian.jira.issue.fields.screen.FieldScreenRendererFactory;
  19. import com.atlassian.jira.jql.resolver.ResolverManager;
  20. import com.atlassian.jira.project.Project;
  21. import com.atlassian.jira.security.JiraAuthenticationContext;
  22. import com.atlassian.jira.security.PermissionManager;
  23. import com.atlassian.jira.security.Permissions;
  24. import com.atlassian.jira.security.roles.ProjectRole;
  25. import com.atlassian.jira.security.roles.ProjectRoleManager;
  26. import com.atlassian.jira.user.util.UserUtil;
  27. import com.atlassian.jira.util.json.JSONArray;
  28. import com.atlassian.jira.util.json.JSONException;
  29. import com.atlassian.jira.util.json.JSONObject;
  30. import com.atlassian.jira.workflow.JiraWorkflow;
  31. import com.atlassian.jira.workflow.WorkflowFunctionUtils;
  32. import com.atlassian.jira.workflow.WorkflowManager;
  33. import com.opensymphony.workflow.Workflow;
  34. import com.opensymphony.workflow.loader.ActionDescriptor;
  35. import com.opensymphony.workflow.loader.WorkflowDescriptor;
  36. import org.apache.commons.lang.StringUtils;
  37. import org.apache.log4j.Logger;
  38. import javax.ws.rs.Consumes;
  39. import javax.ws.rs.GET;
  40. import javax.ws.rs.POST;
  41. import javax.ws.rs.Path;
  42. import javax.ws.rs.PathParam;
  43. import javax.ws.rs.Produces;
  44. import javax.ws.rs.QueryParam;
  45. import javax.ws.rs.core.MediaType;
  46. import javax.ws.rs.core.Response;
  47. import javax.xml.bind.annotation.XmlElement;
  48. import javax.xml.bind.annotation.XmlRootElement;
  49. import java.text.NumberFormat;
  50. import java.util.ArrayList;
  51. import java.util.Collection;
  52. import java.util.Collections;
  53. import java.util.Comparator;
  54. import java.util.HashMap;
  55. import java.util.List;
  56. import java.util.Map;
  57. /**
  58. * Provides same functionality as com.atlassian.jira.rest.v2.issue.IssueResource.doTransition(), but takes in a
  59. */
  60. @Path ("issue")
  61. @Consumes ( { MediaType.APPLICATION_JSON })
  62. @Produces ( { MediaType.APPLICATION_JSON })
  63. public class TransitionAsUserResource
  64. {
  65. private final UserUtil userUtil;
  66. private final JiraAuthenticationContext authContext;
  67. private final IssueManager issueManager;
  68. private final ResolverManager resolverManager;
  69. private final IssueService issueService;
  70. private final ProjectRoleManager projectRoleManager;
  71. private final WorkflowManager workflowManager;
  72. private final FieldScreenRendererFactory fieldScreenRendererFactory;
  73. private static final Logger LOG = Logger.getLogger(TransitionAsUserResource.class);
  74. private final String WALLBOARD_USER = "wallboarduser";
  75. private final PermissionManager permissionManager;
  76. public TransitionAsUserResource(final UserUtil userUtil, final JiraAuthenticationContext authContext, final IssueManager issueManager,
  77. final ResolverManager resolverManager, final IssueService issueService, final ProjectRoleManager projectRoleManager,
  78. final PermissionManager permissionManager, final WorkflowManager workflowManager, final FieldScreenRendererFactory fieldScreenRendererFactory)
  79. {
  80. this.userUtil = userUtil;
  81. this.authContext = authContext;
  82. this.issueManager = issueManager;
  83. this.resolverManager = resolverManager;
  84. this.issueService = issueService;
  85. this.projectRoleManager = projectRoleManager;
  86. this.permissionManager = permissionManager;
  87. this.workflowManager = workflowManager;
  88. this.fieldScreenRendererFactory = fieldScreenRendererFactory;
  89. }
  90. @POST
  91. @Path ("/{issueKey}/transitions")
  92. public Response doTransition(@PathParam ("issueKey") final String issueKey, final String requestBody, @QueryParam ("username") final String username)
  93. {
  94. //If we're being hit by someone not the wallboard user lets return a 403
  95. User oldUser = authContext.getLoggedInUser();
  96. if (!oldUser.getName().equals(WALLBOARD_USER))
  97. {
  98. return Response.status(Response.Status.FORBIDDEN).cacheControl(never()).entity(ErrorCollection.of("Resource can only be accessed by " + WALLBOARD_USER)).build();
  99. }
  100. final User user = userUtil.getUserObject(username);
  101. if (user == null)
  102. {
  103. return Response.status(Response.Status.FORBIDDEN).cacheControl(never()).entity(ErrorCollection.of("no user parameter passed in")).build();
  104. }
  105. //Set the authContext's loggedInUser to the impersonated user
  106. authContext.setLoggedInUser(user);
  107. final Issue issue = getIssueObject(issueKey);
  108. authContext.setLoggedInUser(oldUser);
  109. // Now we need to begin parsing the request JSON that they sent in to us.
  110. final JSONObject json;
  111. try
  112. {
  113. json = new JSONObject(requestBody);
  114. }
  115. catch (JSONException e)
  116. {
  117. throw new RESTException(Response.Status.BAD_REQUEST, "provided json structure invalid");
  118. }
  119. // first check for a transition
  120. if (!json.has("transition"))
  121. {
  122. throw new RESTException(Response.Status.BAD_REQUEST, "no transition provided");
  123. }
  124. final int actionId;
  125. try
  126. {
  127. actionId = json.getInt("transition");
  128. }
  129. catch (JSONException e)
  130. {
  131. throw new RESTException(Response.Status.BAD_REQUEST, "transition provided was not integer");
  132. }
  133. // then the fields
  134. final JSONObject fields = json.optJSONObject("fields");
  135. final IssueInputParameters issueInputParameters;
  136. try
  137. {
  138. issueInputParameters = (fields != null)
  139. ? new IssueInputParametersImpl(jsonToIssueParams(fields))
  140. : new IssueInputParametersImpl();
  141. }
  142. catch (JSONException e)
  143. {
  144. throw new RESTException(Response.Status.BAD_REQUEST, "provided json structure invalid");
  145. }
  146. // finally the (optional) comment
  147. try
  148. {
  149. final Object comment = json.opt("comment");
  150. fillCommentIssueInputParameter(comment, issueInputParameters);
  151. }
  152. catch (IllegalArgumentException e)
  153. {
  154. throw new RESTException(Response.Status.BAD_REQUEST, e.getMessage());
  155. }
  156. catch (JSONException e)
  157. {
  158. throw new RESTException(Response.Status.BAD_REQUEST, "provided json structure invalid");
  159. }
  160. //Update the user for the transition
  161. authContext.setLoggedInUser(user);
  162. final IssueService.TransitionValidationResult validationResult = issueService.validateTransition(user, issue.getId(), actionId, issueInputParameters);
  163. try {
  164. if (!validationResult.isValid())
  165. {
  166. final ErrorCollection errorCollection = ErrorCollection.builder().addErrorCollection(validationResult.getErrorCollection()).build();
  167. throw new RESTException(Response.Status.BAD_REQUEST, errorCollection);
  168. }
  169. else
  170. {
  171. issueService.transition(user, validationResult);
  172. return Response.noContent().build();
  173. }
  174. } finally {
  175. //and reset the user to the old one
  176. authContext.setLoggedInUser(oldUser);
  177. }
  178. }
  179. @GET
  180. @Path ("/{issueKey}/transitions")
  181. public Response getTransitions(@PathParam ("issueKey") final String issueKey, @QueryParam ("username") final String username)
  182. {
  183. //Get the current user
  184. User oldUser = authContext.getLoggedInUser();
  185. if (!oldUser.getName().equals(WALLBOARD_USER))
  186. {
  187. return Response.status(Response.Status.FORBIDDEN).cacheControl(never()).entity(ErrorCollection.of("Resource can only be accessed by " + WALLBOARD_USER)).build();
  188. }
  189. //get the impersonating user
  190. final User user = userUtil.getUserObject(username);
  191. if (user == null)
  192. {
  193. return Response.status(Response.Status.FORBIDDEN).cacheControl(never()).entity(ErrorCollection.of("no user parameter passed in")).build();
  194. }
  195. Map<Integer, ExtendedTransitionsBean> map;
  196. //Impersonate the requested user
  197. authContext.setLoggedInUser(user);
  198. try {
  199. final Issue issue = getIssueObject(issueKey);
  200. final List<ActionDescriptor> actions = loadAvailableActions(user, issue);
  201. Collections.sort(actions, new Comparator<ActionDescriptor>()
  202. {
  203. public int compare(ActionDescriptor o1, ActionDescriptor o2)
  204. {
  205. return getSequenceFromAction(o1).compareTo(getSequenceFromAction(o2));
  206. }
  207. });
  208. map = new HashMap<Integer, ExtendedTransitionsBean>();
  209. for (ActionDescriptor action : actions)
  210. {
  211. final FieldScreenRenderer fieldScreenRenderer = fieldScreenRendererFactory.getFieldScreenRenderer(user, issue, action);
  212. String status = getStatusFromStep(issue, action.getUnconditionalResult().getStep());
  213. final ExtendedTransitionsBean transitions = new ExtendedTransitionsBean(action.getName(), getRequiredFields(fieldScreenRenderer, issue), status);
  214. map.put(action.getId(), transitions);
  215. }
  216. } finally {
  217. //Return to the old user
  218. authContext.setLoggedInUser(oldUser);
  219. }
  220. return Response.ok(map).build();
  221. }
  222. public Collection<TransitionFieldBean> getRequiredFields(final FieldScreenRenderer fieldScreenRenderer, final Issue issue)
  223. {
  224. final Collection<TransitionFieldBean> fields = new ArrayList<TransitionFieldBean>();
  225. for (FieldScreenRenderTab fieldScreenRenderTab : fieldScreenRenderer.getFieldScreenRenderTabs())
  226. {
  227. for (FieldScreenRenderLayoutItem fieldScreenRenderLayoutItem : fieldScreenRenderTab.getFieldScreenRenderLayoutItemsForProcessing())
  228. {
  229. if (fieldScreenRenderLayoutItem.isShow(issue))
  230. {
  231. OrderableField orderableField = fieldScreenRenderLayoutItem.getOrderableField();
  232. // JRA-16112 - This is a hack that is here because the resolution field is "special". You can not
  233. // make the resolution field required and therefore by default the FieldLayoutItem for resolution
  234. // returns false for the isRequired method. This is so that you can not make the resolution field
  235. // required for issue creation. HOWEVER, whenever the resolution system field is shown it is
  236. // required because the edit template does not provide a none option and indicates that it is
  237. // required. THEREFORE, when the field is included on a transition screen we will do a special
  238. // check to make the FieldLayoutItem claim it is required IF we run into the resolution field.
  239. if (IssueFieldConstants.RESOLUTION.equals(orderableField.getId()))
  240. {
  241. fieldScreenRenderLayoutItem =
  242. new FieldScreenRenderLayoutItemImpl(fieldScreenRenderLayoutItem.getFieldScreenLayoutItem(), fieldScreenRenderLayoutItem.getFieldLayoutItem())
  243. {
  244. public boolean isRequired()
  245. {
  246. return true;
  247. }
  248. };
  249. }
  250. String type;
  251. if (orderableField instanceof CustomField)
  252. {
  253. type = ((CustomField) orderableField).getCustomFieldType().getKey();
  254. }
  255. else
  256. {
  257. type = JiraDataTypes.getType(orderableField);
  258. }
  259. final TransitionFieldBean bean = new TransitionFieldBean();
  260. bean.id(orderableField.getId()).required(fieldScreenRenderLayoutItem.isRequired()).type(type);
  261. fields.add(bean);
  262. }
  263. }
  264. }
  265. return fields;
  266. }
  267. private List<ActionDescriptor> loadAvailableActions(User user, Issue issueObject)
  268. {
  269. final Project project = issueObject.getProjectObject();
  270. final List<ActionDescriptor> availableActions = new ArrayList<ActionDescriptor>();
  271. if (issueObject.getWorkflowId() == null)
  272. {
  273. LOG.warn("!!! Issue " + issueObject.getKey() + " has no workflow ID !!! ");
  274. return availableActions;
  275. }
  276. try
  277. {
  278. final Workflow wf = workflowManager.makeWorkflow(user != null ? user.getName() : null);
  279. final WorkflowDescriptor wd = workflowManager.getWorkflow(issueObject).getDescriptor();
  280. final HashMap<String, Object> inputs = new HashMap<String, Object>();
  281. inputs.put("pkey", project.getKey()); // Allows ${project.key} in condition args
  282. inputs.put("issue", issueObject);
  283. // The condition should examine the original issue object - put this in the transientvars
  284. // This is done here as AbstractWorkflow later changes this collection to be an unmodifiable map
  285. inputs.put(WorkflowFunctionUtils.ORIGINAL_ISSUE_KEY, issueObject);
  286. int[] actionIds = wf.getAvailableActions(issueObject.getWorkflowId(), inputs);
  287. for (int actionId : actionIds)
  288. {
  289. final ActionDescriptor action = wd.getAction(actionId);
  290. if (action == null)
  291. {
  292. LOG.error("State of issue [" + issueObject + "] has an action [id=" + actionId +
  293. "] which cannot be found in the workflow descriptor");
  294. }
  295. else
  296. {
  297. availableActions.add(action);
  298. }
  299. }
  300. }
  301. catch (Exception e)
  302. {
  303. LOG.error("Exception thrown while getting available actions", e);
  304. }
  305. return availableActions;
  306. }
  307. private String getStatusFromStep(Issue issue, int stepId)
  308. {
  309. final WorkflowDescriptor wd = workflowManager.getWorkflow(issue).getDescriptor();
  310. return (String) wd.getStep(stepId).getMetaAttributes().get(JiraWorkflow.STEP_STATUS_KEY);
  311. }
  312. private Integer getSequenceFromAction(ActionDescriptor action)
  313. {
  314. if (action == null)
  315. {
  316. return Integer.MAX_VALUE;
  317. }
  318. final Map metaAttributes = action.getMetaAttributes();
  319. if (metaAttributes == null)
  320. {
  321. return Integer.MAX_VALUE;
  322. }
  323. final String value = (String) metaAttributes.get("opsbar-sequence");
  324. if (value == null || StringUtils.isBlank(value) || !StringUtils.isNumeric(value))
  325. {
  326. return Integer.MAX_VALUE;
  327. }
  328. return Integer.valueOf(value);
  329. }
  330. private void fillCommentIssueInputParameter(final Object comment, final IssueInputParameters issueInputParameters)
  331. throws JSONException
  332. {
  333. if (comment != null)
  334. {
  335. if (comment instanceof String)
  336. {
  337. issueInputParameters.setComment((String) comment);
  338. }
  339. else if (comment instanceof JSONObject)
  340. {
  341. final JSONObject commentJson = (JSONObject) comment;
  342. final String body = commentJson.getString("body");
  343. if (commentJson.has("visibility"))
  344. {
  345. final JSONObject visibility = commentJson.getJSONObject("visibility");
  346. final String type = visibility.getString("type");
  347. if (type.equalsIgnoreCase("group"))
  348. {
  349. issueInputParameters.setComment(body, visibility.getString("value"));
  350. }
  351. else if (type.equalsIgnoreCase("role"))
  352. {
  353. final String roleStr = visibility.getString("value");
  354. final ProjectRole role = projectRoleManager.getProjectRole(roleStr);
  355. if (role == null)
  356. {
  357. throw new IllegalArgumentException("Invalid role [" + roleStr + "]");
  358. }
  359. issueInputParameters.setComment(body, role.getId());
  360. }
  361. else
  362. {
  363. throw new IllegalArgumentException(String.format("Unknown visibility type: %s", visibility.getString("type")));
  364. }
  365. }
  366. else
  367. {
  368. issueInputParameters.setComment(body);
  369. }
  370. }
  371. }
  372. }
  373. private Map<String, String[]> jsonToIssueParams(final JSONObject json) throws JSONException
  374. {
  375. Map<String, String[]> map = new HashMap<String, String[]>();
  376. final String[] fields = JSONObject.getNames(json);
  377. if (fields == null)
  378. {
  379. return map;
  380. }
  381. for (String field : fields)
  382. {
  383. final Object value = json.get(field);
  384. // Numbers need special treatment because we need to localize the stringification
  385. if (value instanceof Integer || value instanceof Long || value instanceof Double)
  386. {
  387. final NumberFormat numberFormat = NumberFormat.getInstance(authContext.getLocale());
  388. map.put(field, new String[] { numberFormat.format(value) });
  389. }
  390. else if (value instanceof String)
  391. {
  392. // Wrap a single value in a String[]
  393. if (resolverManager.handles(field))
  394. {
  395. final String resolvedId = resolverManager.getSingleIdFromName(value.toString(), field);
  396. map.put(field, new String[] { resolvedId });
  397. }
  398. else
  399. {
  400. map.put(field, new String[] { value.toString() });
  401. }
  402. }
  403. else if (value instanceof JSONArray)
  404. {
  405. // pull everything out of the JSONArray and put them in a regular Java String[] array
  406. final JSONArray valueArray = (JSONArray) value;
  407. final int size = valueArray.length();
  408. final String[] values = new String[size];
  409. for (int i = 0; i < size; i++)
  410. {
  411. final String currentValue = valueArray.getString(i);
  412. if (resolverManager.handles(field))
  413. {
  414. final String resolvedId = resolverManager.getSingleIdFromName(currentValue, field);
  415. values[i] = resolvedId;
  416. }
  417. else
  418. {
  419. values[i] = currentValue;
  420. }
  421. }
  422. map.put(field, values);
  423. }
  424. }
  425. return map;
  426. }
  427. private Issue getIssueObject(final String issueKey) throws RESTException
  428. {
  429. final MutableIssue issue = issueManager.getIssueObject(issueKey);
  430. if (issue == null)
  431. {
  432. throw new RESTException(ErrorCollection.of("Issue does not exist"));
  433. }
  434. User user = authContext.getLoggedInUser();
  435. if (!permissionManager.hasPermission(Permissions.BROWSE, issue, user))
  436. {
  437. ErrorCollection.Builder errorBuilder = ErrorCollection.Builder.newBuilder().addErrorMessage("User has no permission to see issue");
  438. if (user == null)
  439. {
  440. errorBuilder.addErrorMessage("Login required");
  441. throw new RESTException(Response.Status.UNAUTHORIZED, errorBuilder.build());
  442. }
  443. else
  444. {
  445. throw new RESTException(Response.Status.FORBIDDEN, errorBuilder.build());
  446. }
  447. }
  448. return issue;
  449. }
  450. public static javax.ws.rs.core.CacheControl never()
  451. {
  452. javax.ws.rs.core.CacheControl cacheNever = new javax.ws.rs.core.CacheControl();
  453. cacheNever.setNoStore(true);
  454. cacheNever.setNoCache(true);
  455. return cacheNever;
  456. }
  457. @XmlRootElement
  458. public class ExtendedTransitionsBean
  459. {
  460. @XmlElement
  461. private String name;
  462. @XmlElement
  463. private Collection<TransitionFieldBean> fields;
  464. @XmlElement
  465. private String transitionDestination;
  466. public ExtendedTransitionsBean(final String name, final Collection<TransitionFieldBean> fields, final String transitionDestination)
  467. {
  468. this.name = name;
  469. this.fields = fields;
  470. this.transitionDestination = transitionDestination;
  471. }
  472. }
  473. @XmlRootElement (name = "availableField")
  474. public class TransitionFieldBean
  475. {
  476. @XmlElement
  477. private String id;
  478. @XmlElement
  479. private boolean required;
  480. @XmlElement
  481. private String type;
  482. TransitionFieldBean() {}
  483. public TransitionFieldBean id(final String name)
  484. {
  485. this.id = name;
  486. return this;
  487. }
  488. public TransitionFieldBean required(final boolean required)
  489. {
  490. this.required = required;
  491. return this;
  492. }
  493. public TransitionFieldBean type(final String type)
  494. {
  495. this.type = type;
  496. return this;
  497. }
  498. }
  499. }