/src/main/java/com/atlassian/jconnect/rest/resources/IssueResource.java
Java | 411 lines | 318 code | 52 blank | 41 comment | 49 complexity | bf17ab224e3240d60e8591eb645bd91d MD5 | raw file
- package com.atlassian.jconnect.rest.resources;
-
- import com.atlassian.crowd.embedded.api.User;
- import com.atlassian.jconnect.jira.IssueActivityService;
- import com.atlassian.jconnect.jira.IssueHelper;
- import com.atlassian.jconnect.jira.JMCProjectService;
- import com.atlassian.jconnect.jira.UserHelper;
- import com.atlassian.jconnect.rest.entities.*;
- import com.atlassian.jconnect.util.Either;
- import com.atlassian.jira.exception.CreateException;
- import com.atlassian.jira.issue.CustomFieldManager;
- import com.atlassian.jira.issue.Issue;
- import com.atlassian.jira.issue.MutableIssue;
- import com.atlassian.jira.issue.fields.CustomField;
- import com.atlassian.jira.project.Project;
- import com.atlassian.jira.util.ErrorCollection;
- import com.atlassian.jira.util.IOUtil;
- import com.atlassian.jira.util.json.JSONException;
- import com.atlassian.jira.util.json.JSONObject;
- import com.atlassian.jira.web.util.AttachmentException;
- import com.atlassian.plugins.rest.common.security.AnonymousAllowed;
- import com.opensymphony.workflow.InvalidInputException;
- import org.apache.commons.fileupload.FileItemIterator;
- import org.apache.commons.fileupload.FileItemStream;
- import org.apache.commons.fileupload.FileUploadException;
- import org.apache.commons.fileupload.servlet.ServletFileUpload;
- import org.apache.commons.io.IOUtils;
- import org.ofbiz.core.entity.GenericEntityException;
- import org.slf4j.Logger;
- import org.slf4j.LoggerFactory;
-
- import javax.servlet.http.HttpServletRequest;
- import javax.ws.rs.*;
- import javax.ws.rs.core.Context;
- import javax.ws.rs.core.MediaType;
- import javax.ws.rs.core.Response;
- import java.io.ByteArrayInputStream;
- import java.io.IOException;
- import java.io.InputStream;
- import java.util.*;
-
- import static com.google.common.base.Preconditions.checkNotNull;
-
- /**
- * The issue resource, provides 3 end-points for:
- * <ul>
- * <li> /issue/create creating issues with attachments </li>
- * <li> /issue/updates retrieving updates for a specific uuid </li>
- * <li> /issue/comment/${issue-key} UUID, and commenting on an issue </li>
- * </ul>
- */
- @Path("/issue")
- public class IssueResource {
- private static final Logger log = LoggerFactory.getLogger(IssueResource.class);
-
- private final IssueHelper issueHelper;
- private final UserHelper userHelper;
- private final IssueActivityService issueUpdateService;
- private final CustomFieldManager customFieldManager;
- private final JMCProjectService connectProjectService;
-
-
- /**
- * This is required to map a mobile user, to a JIRA issue.
- */
- private static final String CF_NAME_UUID = "uuid";
-
- public IssueResource(final IssueHelper issueHandler,
- final UserHelper userHelper,
- final IssueActivityService issueUpdateService,
- final CustomFieldManager customFieldManager,
- JMCProjectService connectProjectService) {
- this.issueHelper = issueHandler;
- this.userHelper = userHelper;
- this.issueUpdateService = issueUpdateService;
- this.customFieldManager = customFieldManager;
- this.connectProjectService = connectProjectService;
- }
-
- @POST
- @AnonymousAllowed
- @Consumes(MediaType.MULTIPART_FORM_DATA)
- @Path("create")
- @Produces(MediaType.APPLICATION_JSON)
- public Response createIssue(@QueryParam("project") String project,
- @QueryParam("apikey") String apikey,
- @Context HttpServletRequest request) {
- try {
- final Either<Project, Response.ResponseBuilder> result = lookupProjectByNameOrKey(project, apikey);
- if (result.getRight() != null) {
- return result.getRight().build();
- }
-
- final Map<String, UploadData> data = parseUploadData(request);
-
- // resolve user and create issue
- final UploadData issueData = data.get("issue");
- final IssueEntity issueEntity = parseIssueEntity(issueData);
-
-
-
- if (issueEntity.isCrash() && !connectProjectService.isCrashesEnabledFor(result.getLeft()))
- {
- final IssueWithCommentsEntity issueWithCommentsEntity =
- new IssueWithCommentsEntity("CRASHES-DISABLED",
- "crash-reporting-disabled",
- issueEntity.getSummary(),
- issueEntity.getDescription(),
- new Date(),
- new Date(),
- Collections.<CommentEntity>emptyList(),
- false);
- return Response.ok(issueWithCommentsEntity).build();
- }
-
- final User user = userHelper.getOrCreateJMCSystemUser();
-
- // ensure uuid is set, and there is a uuid customfield configured in JIRA.
- final CustomField uuid = customFieldManager.getCustomFieldObjectByName(CF_NAME_UUID);
-
- // add any attachments
- final UploadData customfields = data.get("customfields");
- final List<CustomField> customFields = new ArrayList<CustomField>();
- final List<Object> values = new ArrayList<Object>();
- if (customfields != null) {
- extractCustomFields(customfields, customFields, values);
- }
-
- // create issue object
- final Issue issue = issueHelper.createIssue(issueEntity, uuid, result.getLeft(), user, customFields, values);
-
- addAnyAttachments(data, user, issue);
-
- // the response should be JSON for the issue we just created.
- final IssueWithCommentsEntity issueWithCommentsEntity =
- new IssueWithCommentsEntity(issue.getKey(),
- issue.getStatusObject().getName(),
- issue.getSummary(),
- issue.getDescription(),
- issue.getCreated(),
- issue.getUpdated(),
- Collections.<CommentEntity>emptyList(),
- false);
- return Response.ok(issueWithCommentsEntity).build();
-
- } catch (IOException e) {
- return handleException(e);
- } catch (AttachmentException e) {
- return handleException(e);
- } catch (GenericEntityException e) {
- return handleException(e);
- } catch (JSONException e) {
- return handleException(e);
- } catch (CreateException e) {
- if (e.getCause() instanceof InvalidInputException) {
- // if the jiraconnectuser is not authorised to create issues in this project -> 401
- return Response.status(Response.Status.UNAUTHORIZED).entity(e.getMessage()).build();
- }
- return handleException(e);
- } catch (FileUploadException e) {
- return handleException(e);
- }
- }
-
- private Response handleException(Exception e) {
- log.error(e.getMessage(), e);
- return Response.status(Response.Status.INTERNAL_SERVER_ERROR).entity(e.getMessage()).build();
- }
-
- @POST
- @AnonymousAllowed
- @Consumes(MediaType.MULTIPART_FORM_DATA)
- @Produces(MediaType.APPLICATION_JSON)
- @Path("/comment/{issueKey}")
- public Response addComment(@PathParam("issueKey") final String issueKey,
- @QueryParam("apikey") String apikey,
- @Context HttpServletRequest request) {
- try {
- // retrieve existing user & issue
- final User user = userHelper.getOrCreateJMCSystemUser();
- final MutableIssue issue = issueHelper.getIssue(checkNotNull(issueKey, "issueKey"));
-
- if (issue == null) {
- return Response.status(Response.Status.NOT_FOUND)
- .entity(String.format("The specified issue %s does not exist", issueKey))
- .build();
- }
- // Once a project is disabled for JMC, then replies are also turned OFF.
- final Either<Project,Response.ResponseBuilder> result = lookupProjectByNameOrKey(issue.getProjectObject().getKey(), apikey);
- if (result.getRight() != null) {
- return result.getRight().build();
- }
- // users should never get here, unless there is a uuid custom field for this project.
- final Map<String, UploadData> data = parseUploadData(request);
- final UploadData issueData = data.get("issue");
- // nb. at the moment we only use the uuid & description fields from the json blob
- final IssueEntity issueEntity = parseIssueEntity(issueData);
-
- final Response.ResponseBuilder errorResponse = checkUUIDIsGood(issueEntity, issue);
- if (errorResponse != null) {
- return errorResponse.entity("You are unauthorized to comment on this issue.").build();
- }
-
- // add comment & any attachments
- final String description = issueEntity.getDescription();
- final ErrorCollection errors = issueHelper.addComment(issue, description, user);
- final Response.ResponseBuilder commentError = checkForCommentErrors(errors, user, issue);
- if (commentError != null) {
- return commentError.build();
- }
- addAnyAttachments(data, user, issue);
-
- try {
- issueHelper.updateIssue(issue, issueEntity, user);
- } catch (Throwable e) {
- log.warn("Could not update issue. Comment and attachments still added though.", e);
- }
-
- final CommentEntity commentEntity = new CommentEntity(user.getName(), true, description, new Date(), issue.getKey());
- return Response.ok(commentEntity).build();
- } catch (GenericEntityException e) {
- return handleException(e);
- } catch (AttachmentException e) {
- return handleException(e);
- } catch (FileUploadException e) {
- return handleException(e);
- } catch (IOException e) {
- return handleException(e);
- } catch (JSONException e) {
- return handleException(e);
- }
- }
-
-
- /**
- * JIRA only GZIP encodes the following mime-types by default:
- * text/.*,application/x-javascript,application/javascript,application/xml,application/xhtml\+xml
- *
- * Returns all issues and comments created by the given uuid.
- * if there were no updates, then an empty JSON array is returned.
- *
- */
- @GET
- @AnonymousAllowed
- @Produces(MediaType.APPLICATION_JSON)
- @Path("/updates")
- public Response getIssuesAndCommentsFor(@QueryParam("project") final String project,
- @QueryParam(CF_NAME_UUID) final String uuid,
- @QueryParam("sinceMillis") final long sinceMillis,
- @QueryParam("apikey") String apikey,
- @Context HttpServletRequest request) {
- final Either<Project, Response.ResponseBuilder> result = lookupProjectByNameOrKey(project, apikey);
- if (result.getRight() != null) {
- return result.getRight().build();
- }
-
- // this ensures application/json is gzipped! see UrlRewriteGzipCompatibilitySelector
- // a typical response here is 3x smaller with compression enabled.
- request.setAttribute("gzipMimeTypes", MediaType.APPLICATION_JSON);
- final IssuesWithCommentsEntity issuesWithComments = issueUpdateService.getIssuesWithCommentsIfUpdatesExists(result.getLeft(), uuid, sinceMillis);
-
- return Response.ok(issuesWithComments).lastModified(new Date()).build();
- }
-
-
- private Either<Project, Response.ResponseBuilder> lookupProjectByNameOrKey(String projectNameOrKey, String apiKey) {
- if (projectNameOrKey == null) {
- return Either.right(Response.status(Response.Status.BAD_REQUEST).entity("project request parameter must be specified."));
- }
-
- Project project = issueHelper.lookupProjectByKey(projectNameOrKey);
- project = (project == null) ? issueHelper.lookupProjectByName(projectNameOrKey) : project;
-
- if (project == null) {
- return Either.right(Response.status(Response.Status.FORBIDDEN).entity(
- "Project: " + projectNameOrKey + " does not exist in this JIRA instance.\n" +
- "Please add a project called " + projectNameOrKey + " before continuing.\n" +
- "Alternatively, configure the name of an existing project in your <JCOCustomDataSource> protocol."));
- }
- if (!connectProjectService.isJiraConnectProject(project)) {
- return Either.right(Response.status(Response.Status.UNAUTHORIZED).entity(
- "JIRA Mobile Connect is not enabled for project: " + project.getKey() +
- ". Please enable JIRA Mobile Connect in the Project Settings in JIRA for this project."));
- }
-
-
- final boolean apiKeyEnabled = connectProjectService.isApiKeyEnabledFor(project);
- if (apiKeyEnabled) {
- final String projectsApiKey = connectProjectService.lookupApiKeyFor(project);
-
- if (projectsApiKey == null) {
- return Either.right(Response.status(Response.Status.FORBIDDEN).entity("Project is missing API Key."));
- }
- if (apiKey == null) {
- return Either.right(Response.status(Response.Status.FORBIDDEN).entity(
- "This request is missing the apikey parameter. Please upgrade the JIRA Mobile Connect SDK."));
- }
- if (!projectsApiKey.equalsIgnoreCase(apiKey)) {
- return Either.right(Response.status(Response.Status.FORBIDDEN).entity(
- "Invalid API Key. '" + apiKey + "' Please ensure it is correctly configured."));
- }
- }
- return Either.left(project);
- }
-
- private Response.ResponseBuilder checkForCommentErrors(ErrorCollection errors, User user, Issue issue) {
- if (errors.hasAnyErrors()) {
- log.warn(String.format("Errors encountered when %s commented on %s:", user.getName(), issue.getKey()));
- final StringBuilder respsonseStr = new StringBuilder();
- for (String msg : errors.getErrorMessages()) {
- respsonseStr.append(msg).append('\n');
- }
- return Response.status(Response.Status.BAD_REQUEST).entity(respsonseStr.toString());
- } else {
- log.debug(String.format("User %s commented on %s", user.getName(), issue.getKey()));
- }
-
- return null;
- }
-
- /**
- * Ensures that issue has a custom uuid field, and that it matches the uuid in IssueEntity
- *
- * @param issueEntity the issue entity sent from the device
- * @param issue the issue retrieved from the database
- * @return a ResponseBuilder if there is an error, or null if the uuid is good
- */
- private Response.ResponseBuilder checkUUIDIsGood(IssueEntity issueEntity, Issue issue) {
- final CustomField uuid = customFieldManager.getCustomFieldObjectByName(CF_NAME_UUID);
- if (uuid == null) {
- return handleMissingUUID();
- }
- final Object fieldValue = issue.getCustomFieldValue(uuid);
- if (fieldValue == null) {
- return Response.status(Response.Status.UNAUTHORIZED);
- }
- final String uuidFiedlValue = (String) fieldValue;
- if (!uuidFiedlValue.equals(issueEntity.getUuid())) {
- return Response.status(Response.Status.UNAUTHORIZED);
- }
- return null;
- }
-
- private Response.ResponseBuilder handleMissingUUID() {
- // handle missing uuid custom uuid. create on the fly?
- return Response.status(Response.Status.FORBIDDEN).entity("Missing uuid.");
- }
-
- private void extractCustomFields(UploadData customfields, List<CustomField> fields, List<Object> values) throws JSONException, IOException {
- final JSONObject json = new JSONObject(IOUtils.toString(customfields.getInputStream(), "UTF-8"));
-
- // so we can do a case-insensitive match, get all custom fields, and then look up in the json
- final List<CustomField> allCustomFields = customFieldManager.getCustomFieldObjects();
- for (CustomField field : allCustomFields) {
- final String fieldNameLower = field.getName().toLowerCase();
- if (json.has(fieldNameLower) || json.has(field.getName())) {
- final String fieldValue = json.has(fieldNameLower) ? json.getString(fieldNameLower) : json.getString(field.getName());
- if (fieldValue != null) {
- fields.add(field);
- values.add(field.getCustomFieldType().getSingularObjectFromString(fieldValue));
- }
- }
- }
- }
-
- private void addAnyAttachments(final Map<String, UploadData> data, final User user, final Issue issue) throws IOException, AttachmentException, GenericEntityException {
- for (Iterator<Map.Entry<String, UploadData>> iterator = data.entrySet().iterator(); iterator.hasNext();) {
- addAttachment(user, issue, iterator.next().getValue());
- }
- }
-
- private void addAttachment(User user, Issue issue, UploadData payload) throws IOException, AttachmentException, GenericEntityException {
- if (isValidAttachment(payload)) {
- issueHelper.addAttachment(issue, payload, user);
- }
- }
-
- private Map<String, UploadData> parseUploadData(final HttpServletRequest request) throws FileUploadException, IOException {
- ServletFileUpload upload = new ServletFileUpload();
- FileItemIterator iterator = upload.getItemIterator(request);
- Map<String, UploadData> data = new HashMap<String, UploadData>();
- while (iterator.hasNext()) {
- try {
- FileItemStream item = iterator.next();
- byte[] bytes = IOUtils.toByteArray(item.openStream());
- InputStream stream = new ByteArrayInputStream(bytes);
- UploadData uploadData = new UploadData(stream, item.getFieldName(), item.getName(), item.getContentType());
- data.put(item.getFieldName(), uploadData);
- } catch (FileItemStream.ItemSkippedException e) {
- log.warn("skipped upload content", e);
- }
- }
- return data;
- }
-
- private IssueEntity parseIssueEntity(UploadData issueData) throws JSONException, IOException {
- final InputStream inputStream = issueData.getInputStream();
- JSONObject obj = new JSONObject(IOUtils.toString(inputStream, "UTF-8"));
- return IssueEntity.fromJSONObj(obj);
- }
-
- private boolean isValidAttachment(final UploadData data) {
- return data != null && data.getName() != null && !isSystemAttachment(data);
- }
-
- private boolean isSystemAttachment(UploadData data) {
- return data.getName().equals("issue") ||
- data.getName().equals("customfields");
- }
-
- }