/atlassian-kinect-wallboard-extensions/src/main/java/com/atlassian/jirawallboard/resttransitionproxy/TransitionAsUserResource.java
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
- package com.atlassian.jirawallboard.resttransitionproxy;
- import com.atlassian.crowd.embedded.api.User;
- import com.atlassian.jira.JiraDataTypes;
- import com.atlassian.jira.bc.issue.IssueService;
- import com.atlassian.jira.issue.Issue;
- import com.atlassian.jira.issue.IssueFieldConstants;
- import com.atlassian.jira.issue.IssueInputParameters;
- import com.atlassian.jira.issue.IssueInputParametersImpl;
- import com.atlassian.jira.issue.IssueManager;
- import com.atlassian.jira.issue.MutableIssue;
- import com.atlassian.jira.issue.fields.CustomField;
- import com.atlassian.jira.issue.fields.FieldManager;
- import com.atlassian.jira.issue.fields.OrderableField;
- import com.atlassian.jira.issue.fields.screen.FieldScreenRenderLayoutItem;
- import com.atlassian.jira.issue.fields.screen.FieldScreenRenderLayoutItemImpl;
- import com.atlassian.jira.issue.fields.screen.FieldScreenRenderTab;
- import com.atlassian.jira.issue.fields.screen.FieldScreenRenderer;
- import com.atlassian.jira.issue.fields.screen.FieldScreenRendererFactory;
- import com.atlassian.jira.jql.resolver.ResolverManager;
- import com.atlassian.jira.project.Project;
- import com.atlassian.jira.security.JiraAuthenticationContext;
- import com.atlassian.jira.security.PermissionManager;
- import com.atlassian.jira.security.Permissions;
- import com.atlassian.jira.security.roles.ProjectRole;
- import com.atlassian.jira.security.roles.ProjectRoleManager;
- import com.atlassian.jira.user.util.UserUtil;
- import com.atlassian.jira.util.json.JSONArray;
- import com.atlassian.jira.util.json.JSONException;
- import com.atlassian.jira.util.json.JSONObject;
- import com.atlassian.jira.workflow.JiraWorkflow;
- import com.atlassian.jira.workflow.WorkflowFunctionUtils;
- import com.atlassian.jira.workflow.WorkflowManager;
- import com.opensymphony.workflow.Workflow;
- import com.opensymphony.workflow.loader.ActionDescriptor;
- import com.opensymphony.workflow.loader.WorkflowDescriptor;
- import org.apache.commons.lang.StringUtils;
- import org.apache.log4j.Logger;
- import javax.ws.rs.Consumes;
- import javax.ws.rs.GET;
- import javax.ws.rs.POST;
- import javax.ws.rs.Path;
- import javax.ws.rs.PathParam;
- import javax.ws.rs.Produces;
- import javax.ws.rs.QueryParam;
- import javax.ws.rs.core.MediaType;
- import javax.ws.rs.core.Response;
- import javax.xml.bind.annotation.XmlElement;
- import javax.xml.bind.annotation.XmlRootElement;
- import java.text.NumberFormat;
- import java.util.ArrayList;
- import java.util.Collection;
- import java.util.Collections;
- import java.util.Comparator;
- import java.util.HashMap;
- import java.util.List;
- import java.util.Map;
- /**
- * Provides same functionality as com.atlassian.jira.rest.v2.issue.IssueResource.doTransition(), but takes in a
- */
- @Path ("issue")
- @Consumes ( { MediaType.APPLICATION_JSON })
- @Produces ( { MediaType.APPLICATION_JSON })
- public class TransitionAsUserResource
- {
- private final UserUtil userUtil;
- private final JiraAuthenticationContext authContext;
- private final IssueManager issueManager;
- private final ResolverManager resolverManager;
- private final IssueService issueService;
- private final ProjectRoleManager projectRoleManager;
- private final WorkflowManager workflowManager;
- private final FieldScreenRendererFactory fieldScreenRendererFactory;
- private static final Logger LOG = Logger.getLogger(TransitionAsUserResource.class);
- private final String WALLBOARD_USER = "wallboarduser";
- private final PermissionManager permissionManager;
- public TransitionAsUserResource(final UserUtil userUtil, final JiraAuthenticationContext authContext, final IssueManager issueManager,
- final ResolverManager resolverManager, final IssueService issueService, final ProjectRoleManager projectRoleManager,
- final PermissionManager permissionManager, final WorkflowManager workflowManager, final FieldScreenRendererFactory fieldScreenRendererFactory)
- {
- this.userUtil = userUtil;
- this.authContext = authContext;
- this.issueManager = issueManager;
- this.resolverManager = resolverManager;
- this.issueService = issueService;
- this.projectRoleManager = projectRoleManager;
- this.permissionManager = permissionManager;
- this.workflowManager = workflowManager;
- this.fieldScreenRendererFactory = fieldScreenRendererFactory;
- }
- @POST
- @Path ("/{issueKey}/transitions")
- public Response doTransition(@PathParam ("issueKey") final String issueKey, final String requestBody, @QueryParam ("username") final String username)
- {
- //If we're being hit by someone not the wallboard user lets return a 403
- User oldUser = authContext.getLoggedInUser();
- if (!oldUser.getName().equals(WALLBOARD_USER))
- {
- return Response.status(Response.Status.FORBIDDEN).cacheControl(never()).entity(ErrorCollection.of("Resource can only be accessed by " + WALLBOARD_USER)).build();
- }
- final User user = userUtil.getUserObject(username);
- if (user == null)
- {
- return Response.status(Response.Status.FORBIDDEN).cacheControl(never()).entity(ErrorCollection.of("no user parameter passed in")).build();
- }
- //Set the authContext's loggedInUser to the impersonated user
- authContext.setLoggedInUser(user);
- final Issue issue = getIssueObject(issueKey);
- authContext.setLoggedInUser(oldUser);
- // Now we need to begin parsing the request JSON that they sent in to us.
- final JSONObject json;
- try
- {
- json = new JSONObject(requestBody);
- }
- catch (JSONException e)
- {
- throw new RESTException(Response.Status.BAD_REQUEST, "provided json structure invalid");
- }
- // first check for a transition
- if (!json.has("transition"))
- {
- throw new RESTException(Response.Status.BAD_REQUEST, "no transition provided");
- }
- final int actionId;
- try
- {
- actionId = json.getInt("transition");
- }
- catch (JSONException e)
- {
- throw new RESTException(Response.Status.BAD_REQUEST, "transition provided was not integer");
- }
- // then the fields
- final JSONObject fields = json.optJSONObject("fields");
- final IssueInputParameters issueInputParameters;
- try
- {
- issueInputParameters = (fields != null)
- ? new IssueInputParametersImpl(jsonToIssueParams(fields))
- : new IssueInputParametersImpl();
- }
- catch (JSONException e)
- {
- throw new RESTException(Response.Status.BAD_REQUEST, "provided json structure invalid");
- }
- // finally the (optional) comment
- try
- {
- final Object comment = json.opt("comment");
- fillCommentIssueInputParameter(comment, issueInputParameters);
- }
- catch (IllegalArgumentException e)
- {
- throw new RESTException(Response.Status.BAD_REQUEST, e.getMessage());
- }
- catch (JSONException e)
- {
- throw new RESTException(Response.Status.BAD_REQUEST, "provided json structure invalid");
- }
- //Update the user for the transition
- authContext.setLoggedInUser(user);
- final IssueService.TransitionValidationResult validationResult = issueService.validateTransition(user, issue.getId(), actionId, issueInputParameters);
- try {
- if (!validationResult.isValid())
- {
- final ErrorCollection errorCollection = ErrorCollection.builder().addErrorCollection(validationResult.getErrorCollection()).build();
- throw new RESTException(Response.Status.BAD_REQUEST, errorCollection);
- }
- else
- {
- issueService.transition(user, validationResult);
- return Response.noContent().build();
- }
- } finally {
- //and reset the user to the old one
- authContext.setLoggedInUser(oldUser);
- }
- }
- @GET
- @Path ("/{issueKey}/transitions")
- public Response getTransitions(@PathParam ("issueKey") final String issueKey, @QueryParam ("username") final String username)
- {
- //Get the current user
- User oldUser = authContext.getLoggedInUser();
- if (!oldUser.getName().equals(WALLBOARD_USER))
- {
- return Response.status(Response.Status.FORBIDDEN).cacheControl(never()).entity(ErrorCollection.of("Resource can only be accessed by " + WALLBOARD_USER)).build();
- }
- //get the impersonating user
- final User user = userUtil.getUserObject(username);
- if (user == null)
- {
- return Response.status(Response.Status.FORBIDDEN).cacheControl(never()).entity(ErrorCollection.of("no user parameter passed in")).build();
- }
- Map<Integer, ExtendedTransitionsBean> map;
- //Impersonate the requested user
- authContext.setLoggedInUser(user);
- try {
- final Issue issue = getIssueObject(issueKey);
- final List<ActionDescriptor> actions = loadAvailableActions(user, issue);
- Collections.sort(actions, new Comparator<ActionDescriptor>()
- {
- public int compare(ActionDescriptor o1, ActionDescriptor o2)
- {
- return getSequenceFromAction(o1).compareTo(getSequenceFromAction(o2));
- }
- });
- map = new HashMap<Integer, ExtendedTransitionsBean>();
- for (ActionDescriptor action : actions)
- {
- final FieldScreenRenderer fieldScreenRenderer = fieldScreenRendererFactory.getFieldScreenRenderer(user, issue, action);
- String status = getStatusFromStep(issue, action.getUnconditionalResult().getStep());
- final ExtendedTransitionsBean transitions = new ExtendedTransitionsBean(action.getName(), getRequiredFields(fieldScreenRenderer, issue), status);
- map.put(action.getId(), transitions);
- }
- } finally {
- //Return to the old user
- authContext.setLoggedInUser(oldUser);
- }
- return Response.ok(map).build();
- }
- public Collection<TransitionFieldBean> getRequiredFields(final FieldScreenRenderer fieldScreenRenderer, final Issue issue)
- {
- final Collection<TransitionFieldBean> fields = new ArrayList<TransitionFieldBean>();
- for (FieldScreenRenderTab fieldScreenRenderTab : fieldScreenRenderer.getFieldScreenRenderTabs())
- {
- for (FieldScreenRenderLayoutItem fieldScreenRenderLayoutItem : fieldScreenRenderTab.getFieldScreenRenderLayoutItemsForProcessing())
- {
- if (fieldScreenRenderLayoutItem.isShow(issue))
- {
- OrderableField orderableField = fieldScreenRenderLayoutItem.getOrderableField();
- // JRA-16112 - This is a hack that is here because the resolution field is "special". You can not
- // make the resolution field required and therefore by default the FieldLayoutItem for resolution
- // returns false for the isRequired method. This is so that you can not make the resolution field
- // required for issue creation. HOWEVER, whenever the resolution system field is shown it is
- // required because the edit template does not provide a none option and indicates that it is
- // required. THEREFORE, when the field is included on a transition screen we will do a special
- // check to make the FieldLayoutItem claim it is required IF we run into the resolution field.
- if (IssueFieldConstants.RESOLUTION.equals(orderableField.getId()))
- {
- fieldScreenRenderLayoutItem =
- new FieldScreenRenderLayoutItemImpl(fieldScreenRenderLayoutItem.getFieldScreenLayoutItem(), fieldScreenRenderLayoutItem.getFieldLayoutItem())
- {
- public boolean isRequired()
- {
- return true;
- }
- };
- }
- String type;
- if (orderableField instanceof CustomField)
- {
- type = ((CustomField) orderableField).getCustomFieldType().getKey();
- }
- else
- {
- type = JiraDataTypes.getType(orderableField);
- }
- final TransitionFieldBean bean = new TransitionFieldBean();
- bean.id(orderableField.getId()).required(fieldScreenRenderLayoutItem.isRequired()).type(type);
- fields.add(bean);
- }
- }
- }
- return fields;
- }
- private List<ActionDescriptor> loadAvailableActions(User user, Issue issueObject)
- {
- final Project project = issueObject.getProjectObject();
- final List<ActionDescriptor> availableActions = new ArrayList<ActionDescriptor>();
- if (issueObject.getWorkflowId() == null)
- {
- LOG.warn("!!! Issue " + issueObject.getKey() + " has no workflow ID !!! ");
- return availableActions;
- }
- try
- {
- final Workflow wf = workflowManager.makeWorkflow(user != null ? user.getName() : null);
- final WorkflowDescriptor wd = workflowManager.getWorkflow(issueObject).getDescriptor();
- final HashMap<String, Object> inputs = new HashMap<String, Object>();
- inputs.put("pkey", project.getKey()); // Allows ${project.key} in condition args
- inputs.put("issue", issueObject);
- // The condition should examine the original issue object - put this in the transientvars
- // This is done here as AbstractWorkflow later changes this collection to be an unmodifiable map
- inputs.put(WorkflowFunctionUtils.ORIGINAL_ISSUE_KEY, issueObject);
- int[] actionIds = wf.getAvailableActions(issueObject.getWorkflowId(), inputs);
- for (int actionId : actionIds)
- {
- final ActionDescriptor action = wd.getAction(actionId);
- if (action == null)
- {
- LOG.error("State of issue [" + issueObject + "] has an action [id=" + actionId +
- "] which cannot be found in the workflow descriptor");
- }
- else
- {
- availableActions.add(action);
- }
- }
- }
- catch (Exception e)
- {
- LOG.error("Exception thrown while getting available actions", e);
- }
- return availableActions;
- }
- private String getStatusFromStep(Issue issue, int stepId)
- {
- final WorkflowDescriptor wd = workflowManager.getWorkflow(issue).getDescriptor();
- return (String) wd.getStep(stepId).getMetaAttributes().get(JiraWorkflow.STEP_STATUS_KEY);
- }
- private Integer getSequenceFromAction(ActionDescriptor action)
- {
- if (action == null)
- {
- return Integer.MAX_VALUE;
- }
- final Map metaAttributes = action.getMetaAttributes();
- if (metaAttributes == null)
- {
- return Integer.MAX_VALUE;
- }
- final String value = (String) metaAttributes.get("opsbar-sequence");
- if (value == null || StringUtils.isBlank(value) || !StringUtils.isNumeric(value))
- {
- return Integer.MAX_VALUE;
- }
- return Integer.valueOf(value);
- }
- private void fillCommentIssueInputParameter(final Object comment, final IssueInputParameters issueInputParameters)
- throws JSONException
- {
- if (comment != null)
- {
- if (comment instanceof String)
- {
- issueInputParameters.setComment((String) comment);
- }
- else if (comment instanceof JSONObject)
- {
- final JSONObject commentJson = (JSONObject) comment;
- final String body = commentJson.getString("body");
- if (commentJson.has("visibility"))
- {
- final JSONObject visibility = commentJson.getJSONObject("visibility");
- final String type = visibility.getString("type");
- if (type.equalsIgnoreCase("group"))
- {
- issueInputParameters.setComment(body, visibility.getString("value"));
- }
- else if (type.equalsIgnoreCase("role"))
- {
- final String roleStr = visibility.getString("value");
- final ProjectRole role = projectRoleManager.getProjectRole(roleStr);
- if (role == null)
- {
- throw new IllegalArgumentException("Invalid role [" + roleStr + "]");
- }
- issueInputParameters.setComment(body, role.getId());
- }
- else
- {
- throw new IllegalArgumentException(String.format("Unknown visibility type: %s", visibility.getString("type")));
- }
- }
- else
- {
- issueInputParameters.setComment(body);
- }
- }
- }
- }
- private Map<String, String[]> jsonToIssueParams(final JSONObject json) throws JSONException
- {
- Map<String, String[]> map = new HashMap<String, String[]>();
- final String[] fields = JSONObject.getNames(json);
- if (fields == null)
- {
- return map;
- }
- for (String field : fields)
- {
- final Object value = json.get(field);
- // Numbers need special treatment because we need to localize the stringification
- if (value instanceof Integer || value instanceof Long || value instanceof Double)
- {
- final NumberFormat numberFormat = NumberFormat.getInstance(authContext.getLocale());
- map.put(field, new String[] { numberFormat.format(value) });
- }
- else if (value instanceof String)
- {
- // Wrap a single value in a String[]
- if (resolverManager.handles(field))
- {
- final String resolvedId = resolverManager.getSingleIdFromName(value.toString(), field);
- map.put(field, new String[] { resolvedId });
- }
- else
- {
- map.put(field, new String[] { value.toString() });
- }
- }
- else if (value instanceof JSONArray)
- {
- // pull everything out of the JSONArray and put them in a regular Java String[] array
- final JSONArray valueArray = (JSONArray) value;
- final int size = valueArray.length();
- final String[] values = new String[size];
- for (int i = 0; i < size; i++)
- {
- final String currentValue = valueArray.getString(i);
- if (resolverManager.handles(field))
- {
- final String resolvedId = resolverManager.getSingleIdFromName(currentValue, field);
- values[i] = resolvedId;
- }
- else
- {
- values[i] = currentValue;
- }
- }
- map.put(field, values);
- }
- }
- return map;
- }
- private Issue getIssueObject(final String issueKey) throws RESTException
- {
- final MutableIssue issue = issueManager.getIssueObject(issueKey);
- if (issue == null)
- {
- throw new RESTException(ErrorCollection.of("Issue does not exist"));
- }
- User user = authContext.getLoggedInUser();
- if (!permissionManager.hasPermission(Permissions.BROWSE, issue, user))
- {
- ErrorCollection.Builder errorBuilder = ErrorCollection.Builder.newBuilder().addErrorMessage("User has no permission to see issue");
- if (user == null)
- {
- errorBuilder.addErrorMessage("Login required");
- throw new RESTException(Response.Status.UNAUTHORIZED, errorBuilder.build());
- }
- else
- {
- throw new RESTException(Response.Status.FORBIDDEN, errorBuilder.build());
- }
- }
- return issue;
- }
- public static javax.ws.rs.core.CacheControl never()
- {
- javax.ws.rs.core.CacheControl cacheNever = new javax.ws.rs.core.CacheControl();
- cacheNever.setNoStore(true);
- cacheNever.setNoCache(true);
- return cacheNever;
- }
- @XmlRootElement
- public class ExtendedTransitionsBean
- {
- @XmlElement
- private String name;
- @XmlElement
- private Collection<TransitionFieldBean> fields;
- @XmlElement
- private String transitionDestination;
- public ExtendedTransitionsBean(final String name, final Collection<TransitionFieldBean> fields, final String transitionDestination)
- {
- this.name = name;
- this.fields = fields;
- this.transitionDestination = transitionDestination;
- }
- }
- @XmlRootElement (name = "availableField")
- public class TransitionFieldBean
- {
- @XmlElement
- private String id;
- @XmlElement
- private boolean required;
- @XmlElement
- private String type;
- TransitionFieldBean() {}
- public TransitionFieldBean id(final String name)
- {
- this.id = name;
- return this;
- }
- public TransitionFieldBean required(final boolean required)
- {
- this.required = required;
- return this;
- }
- public TransitionFieldBean type(final String type)
- {
- this.type = type;
- return this;
- }
- }
- }