diff --git a/README.md b/README.md index ebb8ab6ae..e1fbae3b3 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ To utilize the GitLab API for Java in your project, simply add the following dep ```java dependencies { ... - compile group: 'org.gitlab4j', name: 'gitlab4j-api', version: '4.8.50' + compile group: 'org.gitlab4j', name: 'gitlab4j-api', version: '4.8.53' } ``` @@ -22,7 +22,7 @@ dependencies { org.gitlab4j gitlab4j-api - 4.8.50 + 4.8.53 ``` diff --git a/pom.xml b/pom.xml index 0fb0dfd32..1e64581e7 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ org.gitlab4j gitlab4j-api jar - 4.8.51-SNAPSHOT + 4.8.54-SNAPSHOT GitLab API Java Client GitLab API for Java (gitlab4j-api) provides a full featured Java API for working with GitLab repositories via the GitLab REST API. https://github.com/gmessner/gitlab4j-api diff --git a/src/main/java/org/gitlab4j/api/GitLabApiException.java b/src/main/java/org/gitlab4j/api/GitLabApiException.java index 425b010f7..e39877f5f 100755 --- a/src/main/java/org/gitlab4j/api/GitLabApiException.java +++ b/src/main/java/org/gitlab4j/api/GitLabApiException.java @@ -1,9 +1,19 @@ package org.gitlab4j.api; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; + +import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; import javax.ws.rs.core.Response.StatusType; -import org.gitlab4j.api.models.ErrorMessage; +import org.gitlab4j.api.utils.JacksonJson; + +import com.fasterxml.jackson.databind.JsonNode; /** * This is the exception that will be thrown if any exception occurs while communicating @@ -15,6 +25,7 @@ public class GitLabApiException extends Exception { private StatusType statusInfo; private int httpStatus; private String message; + private Map> validationErrors; /** * Create a GitLabApiException instance with the specified message. @@ -28,7 +39,7 @@ public GitLabApiException(String message) { /** * Create a GitLabApiException instance based on the ClientResponse. - * + * * @param response the JAX-RS response that caused the exception */ public GitLabApiException(Response response) { @@ -38,10 +49,60 @@ public GitLabApiException(Response response) { httpStatus = response.getStatus(); if (response.hasEntity()) { + try { - ErrorMessage errorMessage = response.readEntity(ErrorMessage.class); - message = errorMessage.getMessage(); + String message = response.readEntity(String.class); + this.message = message; + + // Determine what is in the content of the response and process it accordingly + MediaType mediaType = response.getMediaType(); + if (mediaType != null && "json".equals(mediaType.getSubtype())) { + + JsonNode json = JacksonJson.toJsonNode(message); + + // First see if it is a "message", if so it is either a simple message, + // or a Map> of validation errors + JsonNode jsonMessage = json.get("message"); + if (jsonMessage != null) { + + // If the node is an object, then it is validation errors + if (jsonMessage.isObject()) { + + StringBuilder buf = new StringBuilder(); + validationErrors = new HashMap<>(); + Iterator> fields = jsonMessage.fields(); + while(fields.hasNext()) { + + Entry field = fields.next(); + String fieldName = field.getKey(); + List values = new ArrayList<>(); + validationErrors.put(fieldName, values); + for (JsonNode value : field.getValue()) { + values.add(value.asText()); + } + + if (values.size() > 0) { + buf.append((buf.length() > 0 ? ", " : "")).append(fieldName); + } + } + + if (buf.length() > 0) { + this.message = "The following fields have validation errors: " + buf.toString(); + } + + } else { + this.message = jsonMessage.asText(); + } + + } else { + + JsonNode jsonError = json.get("error"); + if (jsonError != null) { + this.message = jsonError.asText(); + } + } + } } catch (Exception ignore) { } @@ -50,7 +111,7 @@ public GitLabApiException(Response response) { /** * Create a GitLabApiException instance based on the exception. - * + * * @param e the Exception to wrap */ public GitLabApiException(Exception e) { @@ -60,7 +121,7 @@ public GitLabApiException(Exception e) { /** * Get the message associated with the exception. - * + * * @return the message associated with the exception */ @Override @@ -71,7 +132,7 @@ public final String getMessage() { /** * Returns the HTTP status reason message, returns null if the * causing error was not an HTTP related exception. - * + * * @return the HTTP status reason message */ public final String getReason() { @@ -80,11 +141,33 @@ public final String getReason() { /** * Returns the HTTP status code that was the cause of the exception. returns 0 if the - * causing error was not an HTTP related exception. - * + * causing error was not an HTTP related exception + * * @return the HTTP status code, returns 0 if the causing error was not an HTTP related exception */ public final int getHttpStatus() { return (httpStatus); } + + /** + * Returns true if this GitLabApiException was caused by validation errors on the GitLab server, + * otherwise returns false. + * + * @return true if this GitLabApiException was caused by validation errors on the GitLab server, + * otherwise returns false + */ + public boolean hasValidationErrors() { + return (validationErrors != null); + } + + /** + * Returns a Map<String, List<String>> instance containing validation errors if this GitLabApiException + * was caused by validation errors on the GitLab server, otherwise returns null. + * + * @return a Map<String, List<String>> instance containing validation errors if this GitLabApiException + * was caused by validation errors on the GitLab server, otherwise returns null + */ + public Map> getValidationErrors() { + return (validationErrors); + } } diff --git a/src/main/java/org/gitlab4j/api/GroupApi.java b/src/main/java/org/gitlab4j/api/GroupApi.java index 256c27e77..1e1283f93 100644 --- a/src/main/java/org/gitlab4j/api/GroupApi.java +++ b/src/main/java/org/gitlab4j/api/GroupApi.java @@ -347,7 +347,7 @@ public Group addGroup(Group group) throws GitLabApiException { .withParam("name", group.getName()) .withParam("path", group.getPath()) .withParam("description", group.getDescription()) - .withParam("visibility", group.getDescription()) + .withParam("visibility", group.getVisibility()) .withParam("lfs_enabled", group.getLfsEnabled()) .withParam("request_access_enabled", group.getRequestAccessEnabled()) .withParam("parent_id", isApiVersion(ApiVersion.V3) ? null : group.getParentId()); diff --git a/src/main/java/org/gitlab4j/api/IssuesApi.java b/src/main/java/org/gitlab4j/api/IssuesApi.java index e3f14d764..f4478192f 100644 --- a/src/main/java/org/gitlab4j/api/IssuesApi.java +++ b/src/main/java/org/gitlab4j/api/IssuesApi.java @@ -34,6 +34,7 @@ import org.gitlab4j.api.models.Duration; import org.gitlab4j.api.models.Issue; import org.gitlab4j.api.models.IssueFilter; +import org.gitlab4j.api.models.MergeRequest; import org.gitlab4j.api.models.TimeStats; import org.gitlab4j.api.utils.DurationUtils; @@ -621,4 +622,52 @@ public Optional getOptionalTimeTrackingStats(Integer projectId, Integ return (GitLabApi.createOptionalFromException(glae)); } } + + /** + * Get list containing all the merge requests that will close issue when merged. + * + * GET /projects/:id/issues/:issue_iid/closed_by + * + * @param projectIdOrPath id, path of the project, or a Project instance holding the project ID or path + * @param issueIid the internal ID of a project's issue + * @return a List containing all the merge requests what will close the issue when merged. + * @throws GitLabApiException if any exception occurs + */ + public List getClosedByMergeRequests(Object projectIdOrPath, Integer issueIid) throws GitLabApiException { + return (getClosedByMergeRequests(projectIdOrPath, issueIid, 1, getDefaultPerPage())); + } + + /** + * Get list containing all the merge requests that will close issue when merged. + * + * GET /projects/:id/issues/:issue_iid/closed_by + * + * @param projectIdOrPath id, path of the project, or a Project instance holding the project ID or path + * @param issueIid the internal ID of a project's issue + * @param page the page to get + * @param perPage the number of issues per page + * @return a List containing all the merge requests what will close the issue when merged. + * @throws GitLabApiException if any exception occurs + */ + public List getClosedByMergeRequests(Object projectIdOrPath, Integer issueIid, int page, int perPage) throws GitLabApiException { + Response response = get(Response.Status.OK, getPageQueryParams(page, perPage), + "projects", getProjectIdOrPath(projectIdOrPath), "issues", issueIid, "closed_by"); + return (response.readEntity(new GenericType>() { })); + } + + /** + * Get a Pager containing all the merge requests that will close issue when merged. + * + * GET /projects/:id/issues/:issue_iid/closed_by + * + * @param projectIdOrPath id, path of the project, or a Project instance holding the project ID or path + * @param issueIid the internal ID of a project's issue + * @param itemsPerPage the number of Issue instances that will be fetched per page + * @return a Pager containing all the issues that would be closed by merging the provided merge request + * @throws GitLabApiException if any exception occurs + */ + public Pager getClosedByMergeRequests(Object projectIdOrPath, Integer issueIid, int itemsPerPage) throws GitLabApiException { + return new Pager(this, MergeRequest.class, itemsPerPage, null, + "projects", getProjectIdOrPath(projectIdOrPath), "issues", issueIid, "closed_by"); + } } diff --git a/src/main/java/org/gitlab4j/api/MergeRequestApi.java b/src/main/java/org/gitlab4j/api/MergeRequestApi.java index 1e461a8c5..cd15cf3c5 100644 --- a/src/main/java/org/gitlab4j/api/MergeRequestApi.java +++ b/src/main/java/org/gitlab4j/api/MergeRequestApi.java @@ -1,17 +1,19 @@ package org.gitlab4j.api; -import org.gitlab4j.api.GitLabApi.ApiVersion; -import org.gitlab4j.api.models.Commit; -import org.gitlab4j.api.models.MergeRequest; -import org.gitlab4j.api.models.MergeRequestFilter; -import org.gitlab4j.api.models.Participant; +import java.util.List; +import java.util.Optional; import javax.ws.rs.core.Form; import javax.ws.rs.core.GenericType; import javax.ws.rs.core.MultivaluedMap; import javax.ws.rs.core.Response; -import java.util.List; -import java.util.Optional; + +import org.gitlab4j.api.GitLabApi.ApiVersion; +import org.gitlab4j.api.models.Commit; +import org.gitlab4j.api.models.Issue; +import org.gitlab4j.api.models.MergeRequest; +import org.gitlab4j.api.models.MergeRequestFilter; +import org.gitlab4j.api.models.Participant; /** * This class implements the client side API for the GitLab merge request calls. @@ -785,4 +787,52 @@ public List getParticipants(Integer projectId, Integer mergeRequest public Pager getParticipants(Integer projectId, Integer mergeRequestIid, int itemsPerPage) throws GitLabApiException { return new Pager(this, Participant.class, itemsPerPage, null, "projects", projectId, "merge_requests", mergeRequestIid, "participants"); } + + /** + * Get list containing all the issues that would be closed by merging the provided merge requestt. + * + *
GitLab Endpoint: GET /projects/:id/merge_requests/:merge_request_iid/closes_issues
+ * + * @param projectIdOrPath id, path of the project, or a Project instance holding the project ID or path + * @param mergeRequestIid the IID of the merge request to get the closes issues for + * @return a List containing all the issues that would be closed by merging the provided merge request + * @throws GitLabApiException if any exception occurs + */ + public List getClosesIssues(Object projectIdOrPath, Integer mergeRequestIid) throws GitLabApiException { + return (getClosesIssues(projectIdOrPath, mergeRequestIid, 1, getDefaultPerPage())); + } + + /** + * Get list containing all the issues that would be closed by merging the provided merge request. + * + *
GitLab Endpoint: GET /projects/:id/merge_requests/:merge_request_iid/closes_issues
+ * + * @param projectIdOrPath id, path of the project, or a Project instance holding the project ID or path + * @param mergeRequestIid the IID of the merge request to get the closes issues for + * @param page the page to get + * @param perPage the number of issues per page + * @return a List containing all the issues that would be closed by merging the provided merge request + * @throws GitLabApiException if any exception occurs + */ + public List getClosesIssues(Object projectIdOrPath, Integer mergeRequestIid, int page, int perPage) throws GitLabApiException { + Response response = get(Response.Status.OK, getPageQueryParams(page, perPage), + "projects", getProjectIdOrPath(projectIdOrPath), "merge_requests", mergeRequestIid, "closes_issues"); + return (response.readEntity(new GenericType>() { })); + } + + /** + * Get a Pager containing all the issues that would be closed by merging the provided merge request. + * + *
GitLab Endpoint: GET /projects/:id/merge_requests/:merge_request_iid/closes_issues
+ * + * @param projectIdOrPath id, path of the project, or a Project instance holding the project ID or path + * @param mergeRequestIid the IID of the merge request to get the closes issues for + * @param itemsPerPage the number of Issue instances that will be fetched per page + * @return a Pager containing all the issues that would be closed by merging the provided merge request + * @throws GitLabApiException if any exception occurs + */ + public Pager getClosesIssues(Object projectIdOrPath, Integer mergeRequestIid, int itemsPerPage) throws GitLabApiException { + return new Pager(this, Issue.class, itemsPerPage, null, + "projects", getProjectIdOrPath(projectIdOrPath), "merge_requests", mergeRequestIid, "closes_issues"); + } } diff --git a/src/main/java/org/gitlab4j/api/ProjectApi.java b/src/main/java/org/gitlab4j/api/ProjectApi.java index 86ff54911..19902edac 100644 --- a/src/main/java/org/gitlab4j/api/ProjectApi.java +++ b/src/main/java/org/gitlab4j/api/ProjectApi.java @@ -655,6 +655,8 @@ public Project createProject(Project project) throws GitLabApiException { * repositoryStorage (optional) - Which storage shard the repository is on. Available only to admins * approvalsBeforeMerge (optional) - How many approvers should approve merge request by default * printingMergeRequestLinkEnabled (optional) - Show link to create/view merge request when pushing from the command line + * resolveOutdatedDiffDiscussions (optional) - Automatically resolve merge request diffs discussions on lines changed with a push + * initialize_with_readme (optional) - Initialize project with README file * * @param project the Project instance with the configuration for the new project * @param importUrl the URL to import the repository from @@ -696,7 +698,9 @@ public Project createProject(Project project, String importUrl) throws GitLabApi .withParam("repository_storage", project.getRepositoryStorage()) .withParam("approvals_before_merge", project.getApprovalsBeforeMerge()) .withParam("import_url", importUrl) - .withParam("printing_merge_request_link_enabled", project.getPrintingMergeRequestLinkEnabled()); + .withParam("printing_merge_request_link_enabled", project.getPrintingMergeRequestLinkEnabled()) + .withParam("resolve_outdated_diff_discussions", project.getResolveOutdatedDiffDiscussions()) + .withParam("initialize_with_readme", project.getInitializeWithReadme()); if (isApiVersion(ApiVersion.V3)) { boolean isPublic = (project.getPublic() != null ? project.getPublic() : project.getVisibility() == Visibility.PUBLIC); @@ -889,12 +893,14 @@ public Project createProject(String name, Integer namespaceId, String descriptio * repositoryStorage (optional) - Which storage shard the repository is on. Available only to admins * approvalsBeforeMerge (optional) - How many approvers should approve merge request by default * printingMergeRequestLinkEnabled (optional) - Show link to create/view merge request when pushing from the command line + * resolveOutdatedDiffDiscussions (optional) - Automatically resolve merge request diffs discussions on lines changed with a push * * NOTE: The following parameters specified by the GitLab API edit project are not supported: * import_url * tag_list array * avatar * ci_config_path + * initialize_with_readme * * @param project the Project instance with the configuration for the new project * @return a Project instance with the newly updated project info @@ -936,7 +942,8 @@ public Project updateProject(Project project) throws GitLabApiException { .withParam("request_access_enabled", project.getRequestAccessEnabled()) .withParam("repository_storage", project.getRepositoryStorage()) .withParam("approvals_before_merge", project.getApprovalsBeforeMerge()) - .withParam("printing_merge_request_link_enabled", project.getPrintingMergeRequestLinkEnabled()); + .withParam("printing_merge_request_link_enabled", project.getPrintingMergeRequestLinkEnabled()) + .withParam("resolve_outdated_diff_discussions", project.getResolveOutdatedDiffDiscussions()); if (isApiVersion(ApiVersion.V3)) { formData.withParam("visibility_level", project.getVisibilityLevel()); diff --git a/src/main/java/org/gitlab4j/api/models/ErrorMessage.java b/src/main/java/org/gitlab4j/api/models/ErrorMessage.java deleted file mode 100644 index 140fc7e20..000000000 --- a/src/main/java/org/gitlab4j/api/models/ErrorMessage.java +++ /dev/null @@ -1,21 +0,0 @@ - -package org.gitlab4j.api.models; - -import javax.xml.bind.annotation.XmlAccessType; -import javax.xml.bind.annotation.XmlAccessorType; -import javax.xml.bind.annotation.XmlRootElement; - -@XmlRootElement -@XmlAccessorType(XmlAccessType.FIELD) -public class ErrorMessage { - - private String message; - - public String getMessage() { - return this.message; - } - - public void setMessage(String message) { - this.message = message; - } -} diff --git a/src/main/java/org/gitlab4j/api/models/Project.java b/src/main/java/org/gitlab4j/api/models/Project.java index 1819c9b70..11a643891 100644 --- a/src/main/java/org/gitlab4j/api/models/Project.java +++ b/src/main/java/org/gitlab4j/api/models/Project.java @@ -85,7 +85,9 @@ public String toString() { private String webUrl; private Boolean wikiEnabled; private Boolean printingMergeRequestLinkEnabled; + private Boolean resolveOutdatedDiffDiscussions; private ProjectStatistics statistics; + private Boolean initializeWithReadme; public Integer getApprovalsBeforeMerge() { return approvalsBeforeMerge; @@ -593,6 +595,32 @@ public Project withPrintingMergeRequestLinkEnabled(Boolean printingMergeRequestL return (this); } + public Boolean getResolveOutdatedDiffDiscussions() { + return resolveOutdatedDiffDiscussions; + } + + public void setResolveOutdatedDiffDiscussions(Boolean resolveOutdatedDiffDiscussions) { + this.resolveOutdatedDiffDiscussions = resolveOutdatedDiffDiscussions; + } + + public Project withResolveOutdatedDiffDiscussions(boolean resolveOutdatedDiffDiscussions) { + this.resolveOutdatedDiffDiscussions = resolveOutdatedDiffDiscussions; + return (this); + } + + public Boolean getInitializeWithReadme() { + return initializeWithReadme; + } + + public void setInitializeWithReadme(Boolean initializeWithReadme) { + this.initializeWithReadme = initializeWithReadme; + } + + public Project withInitializeWithReadme(boolean initializeWithReadme) { + this.initializeWithReadme = initializeWithReadme; + return (this); + } + public ProjectStatistics getStatistics() { return statistics; } diff --git a/src/main/java/org/gitlab4j/api/utils/JacksonJson.java b/src/main/java/org/gitlab4j/api/utils/JacksonJson.java index dfd2b54ed..6958978ac 100644 --- a/src/main/java/org/gitlab4j/api/utils/JacksonJson.java +++ b/src/main/java/org/gitlab4j/api/utils/JacksonJson.java @@ -243,4 +243,15 @@ private static class JacksonJsonSingletonHelper { public static String toJsonString(final T object) { return (JacksonJsonSingletonHelper.JACKSON_JSON.marshal(object)); } + + /** + * Parse the provided String into a JsonNode instance. + * + * @param jsonString a String containing JSON to parse + * @return a JsonNode with the String parsed into a JSON tree + * @throws IOException if any IO error occurs + */ + public static JsonNode toJsonNode(String jsonString) throws IOException { + return (JacksonJsonSingletonHelper.JACKSON_JSON.objectMapper.readTree(jsonString)); + } } diff --git a/src/test/java/org/gitlab4j/api/TestGitLabApiException.java b/src/test/java/org/gitlab4j/api/TestGitLabApiException.java new file mode 100644 index 000000000..69d2d93a0 --- /dev/null +++ b/src/test/java/org/gitlab4j/api/TestGitLabApiException.java @@ -0,0 +1,135 @@ +package org.gitlab4j.api; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; +import static org.junit.Assume.assumeTrue; + +import java.util.Arrays; +import java.util.List; +import java.util.Map; + +import org.gitlab4j.api.GitLabApi.ApiVersion; +import org.gitlab4j.api.models.Project; +import org.gitlab4j.api.models.Visibility; +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; + +/** + * In order for these tests to run you must set the following properties in ~/test-gitlab4j.properties + * + * TEST_NAMESPACE + * TEST_HOST_URL + * TEST_PRIVATE_TOKEN + * + * If any of the above are NULL, all tests in this class will be skipped. + */ +public class TestGitLabApiException { + + // The following needs to be set to your test repository + private static final String TEST_NAMESPACE; + private static final String TEST_HOST_URL; + private static final String TEST_PRIVATE_TOKEN; + + + static { + TEST_NAMESPACE = TestUtils.getProperty("TEST_NAMESPACE"); + TEST_HOST_URL = TestUtils.getProperty("TEST_HOST_URL"); + TEST_PRIVATE_TOKEN = TestUtils.getProperty("TEST_PRIVATE_TOKEN"); + } + + private static final String TEST_PROJECT_NAME_DUPLICATE = "test-gitlab4j-create-project-duplicate"; + private static GitLabApi gitLabApi; + + public TestGitLabApiException() { + super(); + } + + @BeforeClass + public static void setup() { + + String problems = ""; + if (TEST_NAMESPACE == null || TEST_NAMESPACE.trim().isEmpty()) { + problems += "TEST_NAMESPACE cannot be empty\n"; + } + + if (TEST_HOST_URL == null || TEST_HOST_URL.trim().isEmpty()) { + problems += "TEST_HOST_URL cannot be empty\n"; + } + + if (TEST_PRIVATE_TOKEN == null || TEST_PRIVATE_TOKEN.trim().isEmpty()) { + problems += "TEST_PRIVATE_TOKEN cannot be empty\n"; + } + + if (problems.isEmpty()) { + gitLabApi = new GitLabApi(ApiVersion.V4, TEST_HOST_URL, TEST_PRIVATE_TOKEN); + } else { + System.err.print(problems); + } + + deleteAllTestProjects(); + } + + @AfterClass + public static void teardown() throws GitLabApiException { + deleteAllTestProjects(); + } + + private static void deleteAllTestProjects() { + if (gitLabApi != null) { + try { + Project project = gitLabApi.getProjectApi().getProject(TEST_NAMESPACE, TEST_PROJECT_NAME_DUPLICATE); + gitLabApi.getProjectApi().deleteProject(project); + } catch (GitLabApiException ignore) {} + } + } + + @Before + public void beforeMethod() { + assumeTrue(gitLabApi != null); + } + + @Test + public void testNotFoundError() throws GitLabApiException { + + try { + gitLabApi.getProjectApi().getProject(123456789); + fail("GitLabApiException not thrown"); + } catch (GitLabApiException gae) { + assertFalse(gae.hasValidationErrors()); + assertEquals(404, gae.getHttpStatus()); + assertTrue(gae.getMessage().contains("404")); + } + } + + @Test + public void testValidationErrors() throws GitLabApiException { + + Project project = new Project() + .withName(TEST_PROJECT_NAME_DUPLICATE) + .withDescription("GitLab4J test project.") + .withIssuesEnabled(true) + .withMergeRequestsEnabled(true) + .withWikiEnabled(true) + .withSnippetsEnabled(true) + .withVisibility(Visibility.PUBLIC) + .withTagList(Arrays.asList("tag1", "tag2")); + + Project newProject = gitLabApi.getProjectApi().createProject(project); + assertNotNull(newProject); + + try { + newProject = gitLabApi.getProjectApi().createProject(project); + fail("GitLabApiException not thrown"); + } catch (GitLabApiException gae) { + assertTrue(gae.hasValidationErrors()); + Map> validationErrors = gae.getValidationErrors(); + assertNotNull(validationErrors); + assertFalse(validationErrors.isEmpty()); + } + } +}