diff --git a/src/main/java/org/gitlab4j/api/GitLabApi.java b/src/main/java/org/gitlab4j/api/GitLabApi.java index c03defb7b..17ffc22c3 100644 --- a/src/main/java/org/gitlab4j/api/GitLabApi.java +++ b/src/main/java/org/gitlab4j/api/GitLabApi.java @@ -96,6 +96,7 @@ public String getApiNamespace() { private SystemHooksApi systemHooksApi; private TagsApi tagsApi; private TodosApi todosApi; + private TopicsApi topicsApi; private UserApi userApi; private WikisApi wikisApi; private KeysApi keysApi; @@ -1463,7 +1464,7 @@ public ReleaseLinksApi getReleaseLinksApi() { return releaseLinksApi; } - + /** * Gets the ReleasesApi instance owned by this GitLabApi instance. The ReleasesApi is used * to perform all release related API calls. @@ -1654,6 +1655,25 @@ public TagsApi getTagsApi() { return (tagsApi); } + /** + * Gets the TagsApi instance owned by this GitLabApi instance. The TagsApi is used + * to perform all tag and release related API calls. + * + * @return the TagsApi instance owned by this GitLabApi instance + */ + public TopicsApi getTopicsApi() { + + if (topicsApi == null) { + synchronized (this) { + if (topicsApi == null) { + topicsApi = new TopicsApi(this); + } + } + } + + return (topicsApi); + } + /** * Gets the SnippetsApi instance owned by this GitLabApi instance. The SnippetsApi is used * to perform all snippet related API calls. diff --git a/src/main/java/org/gitlab4j/api/GitLabApiClient.java b/src/main/java/org/gitlab4j/api/GitLabApiClient.java index 169e185ce..ea11c8119 100755 --- a/src/main/java/org/gitlab4j/api/GitLabApiClient.java +++ b/src/main/java/org/gitlab4j/api/GitLabApiClient.java @@ -41,6 +41,7 @@ import org.glassfish.jersey.jackson.JacksonFeature; import org.glassfish.jersey.media.multipart.BodyPart; import org.glassfish.jersey.media.multipart.Boundary; +import org.glassfish.jersey.media.multipart.FormDataBodyPart; import org.glassfish.jersey.media.multipart.FormDataMultiPart; import org.glassfish.jersey.media.multipart.MultiPart; import org.glassfish.jersey.media.multipart.MultiPartFeature; @@ -264,7 +265,7 @@ public void close() { * * @param logger the Logger instance to log to * @param level the logging level (SEVERE, WARNING, INFO, CONFIG, FINE, FINER, FINEST) - * @param maxEntitySize maximum number of entity bytes to be logged. When logging if the maxEntitySize + * @param maxEntityLength maximum number of entity bytes to be logged. When logging if the maxEntitySize * is reached, the entity logging will be truncated at maxEntitySize and "...more..." will be added at * the end of the log entry. If maxEntitySize is <= 0, entity logging will be disabled * @param maskedHeaderNames a list of header names that should have the values masked @@ -691,7 +692,11 @@ protected Response putUpload(String name, File fileToUpload, Object... pathArgs) protected Response putUpload(String name, File fileToUpload, URL url) throws IOException { try (MultiPart multiPart = new FormDataMultiPart()) { - multiPart.bodyPart(new FileDataBodyPart(name, fileToUpload, MediaType.APPLICATION_OCTET_STREAM_TYPE)); + if(fileToUpload == null) { + multiPart.bodyPart(new FormDataBodyPart(name, "", MediaType.APPLICATION_OCTET_STREAM_TYPE)); + } else { + multiPart.bodyPart(new FileDataBodyPart(name, fileToUpload, MediaType.APPLICATION_OCTET_STREAM_TYPE)); + } final Entity entity = Entity.entity(multiPart, Boundary.addBoundary(multiPart.getMediaType())); return (invocation(url, null).put(entity)); } diff --git a/src/main/java/org/gitlab4j/api/ProjectApi.java b/src/main/java/org/gitlab4j/api/ProjectApi.java index 848f6bdca..45ce7c710 100644 --- a/src/main/java/org/gitlab4j/api/ProjectApi.java +++ b/src/main/java/org/gitlab4j/api/ProjectApi.java @@ -1043,6 +1043,10 @@ public Project createProject(Project project, String importUrl) throws GitLabApi if (project.getTagList() != null && !project.getTagList().isEmpty()) { throw new IllegalArgumentException("GitLab API v3 does not support tag lists when creating projects"); } + + if (project.getTopics() != null && !project.getTopics().isEmpty()) { + throw new IllegalArgumentException("GitLab API v3 does not support topics when creating projects"); + } } else { Visibility visibility = (project.getVisibility() != null ? project.getVisibility() : project.getPublic() == Boolean.TRUE ? Visibility.PUBLIC : null); @@ -1051,6 +1055,10 @@ public Project createProject(Project project, String importUrl) throws GitLabApi if (project.getTagList() != null && !project.getTagList().isEmpty()) { formData.withParam("tag_list", String.join(",", project.getTagList())); } + + if (project.getTopics() != null && !project.getTopics().isEmpty()) { + formData.withParam("topics", String.join(",", project.getTopics())); + } } Response response = post(Response.Status.CREATED, formData, "projects"); @@ -1314,6 +1322,10 @@ public Project updateProject(Project project) throws GitLabApiException { if (project.getTagList() != null && !project.getTagList().isEmpty()) { throw new IllegalArgumentException("GitLab API v3 does not support tag lists when updating projects"); } + + if (project.getTopics() != null && !project.getTopics().isEmpty()) { + throw new IllegalArgumentException("GitLab API v3 does not support topics when updating projects"); + } } else { Visibility visibility = (project.getVisibility() != null ? project.getVisibility() : project.getPublic() == Boolean.TRUE ? Visibility.PUBLIC : null); @@ -1322,6 +1334,10 @@ public Project updateProject(Project project) throws GitLabApiException { if (project.getTagList() != null && !project.getTagList().isEmpty()) { formData.withParam("tag_list", String.join(",", project.getTagList())); } + + if (project.getTopics() != null && !project.getTopics().isEmpty()) { + formData.withParam("topics", String.join(",", project.getTopics())); + } } Response response = putWithFormData(Response.Status.OK, formData, "projects", projectIdentifier); diff --git a/src/main/java/org/gitlab4j/api/TopicsApi.java b/src/main/java/org/gitlab4j/api/TopicsApi.java new file mode 100644 index 000000000..c6d41ca93 --- /dev/null +++ b/src/main/java/org/gitlab4j/api/TopicsApi.java @@ -0,0 +1,193 @@ +package org.gitlab4j.api; + +import org.gitlab4j.api.models.Topic; +import org.gitlab4j.api.models.TopicParams; + +import javax.ws.rs.core.GenericType; +import javax.ws.rs.core.Response; +import java.io.File; +import java.util.List; +import java.util.Optional; +import java.util.stream.Stream; + +public class TopicsApi extends AbstractApi{ + + public TopicsApi(GitLabApi gitLabApi) { + super(gitLabApi); + } + + /** + *

Get a list of Topics.

+ * + * WARNING: Do not use this method to fetch Topics from https://gitlab.com, + * gitlab.com has many 1,000's of public topics and it will a long time to fetch all of them. + * Instead use {@link #getTopics(int itemsPerPage)} which will return a Pager of Topic instances. + * + *
GitLab Endpoint: GET /topics
+ * + * @return the list of topics viewable by the authenticated user + * @throws GitLabApiException if any exception occurs + */ + public List getTopics() throws GitLabApiException { + return (getTopics(getDefaultPerPage()).all()); + } + + /** + * Get a list of topics in the specified page range. + * + *
GitLab Endpoint: GET /topics
+ * + * @param page the page to get + * @param perPage the number of Topic instances per page + * @return the list of topics + * @throws GitLabApiException if any exception occurs + */ + public List getTopics(int page, int perPage) throws GitLabApiException { + Response response = get(Response.Status.OK, getPageQueryParams(page, perPage), "topics"); + return (response.readEntity(new GenericType>() {})); + } + + /** + * Get a Pager of topics. + * + *
GitLab Endpoint: GET /topics
+ * + * @param itemsPerPage the number of Topic instances that will be fetched per page + * @return the pager of topics + * @throws GitLabApiException if any exception occurs + */ + public Pager getTopics(int itemsPerPage) throws GitLabApiException { + return (new Pager(this, Topic.class, itemsPerPage, null, "topics")); + } + + /** + * Get a Stream of topics. + * + *
GitLab Endpoint: GET /topics
+ * + * @return the stream of topics + * @throws GitLabApiException if any exception occurs + */ + public Stream getTopicsStream() throws GitLabApiException { + return (getTopics(getDefaultPerPage()).stream()); + } + + /** + * Get all details of a topic. + * + *
GitLab Endpoint: GET /topics/:id
+ * + * @param id the topic ID + * @return the topic for the specified topic id + * @throws GitLabApiException if any exception occurs + */ + public Topic getTopic(Integer id) throws GitLabApiException { + Response response = get(Response.Status.OK, null, "topics", id); + return (response.readEntity(Topic.class)); + } + + /** + * Get all details of a topic as an Optional instance. + * + *
GitLab Endpoint: GET /topics/:id
+ * + * @param id the topic ID + * @return the Topic for the specified topic id as an Optional instance + */ + public Optional getOptionalTopic(Integer id) { + try { + return (Optional.ofNullable(getTopic(id))); + } catch (GitLabApiException glae) { + return (GitLabApi.createOptionalFromException(glae)); + } + } + + /** + * Creates a new Topic. Available only for users who can create topics. + * + *
GitLab Endpoint: POST /topics
+ * + * @param params a TopicParams instance holding the parameters for the topic creation + * @return the created Topic instance + * @throws GitLabApiException if any exception occurs + */ + public Topic createTopic(TopicParams params) throws GitLabApiException { + Response response = post(Response.Status.CREATED, params.getForm(true), "topics"); + return (response.readEntity(Topic.class)); + } + + /** + * Update a project topic. + * + *
GitLab Endpoint: PUT /topics/:id
+ * + * @param id the topic id + * @param params a TopicParams instance holding the properties to Update + * @return the updated Topic instance + * @throws GitLabApiException at any exception + */ + public Topic updateTopic(Integer id, TopicParams params) throws GitLabApiException { + Response response = putWithFormData(Response.Status.OK, + params.getForm(false), "topics", id); + return (response.readEntity(Topic.class)); + } + + /** + * Uploads and sets the topic's avatar for the specified topic. + * + *
GitLab Endpoint: PUT /topics/:id
+ * + * @param id the topic in the form of an Integer + * @param avatarFile the File instance of the avatar file to upload + * @return the updated Topic instance + * @throws GitLabApiException if any exception occurs + */ + public Topic updateTopicAvatar(final Integer id, File avatarFile) throws GitLabApiException { + Response response = putUpload(Response.Status.OK, "avatar", avatarFile, "topics", id); + return (response.readEntity(Topic.class)); + } + + /** + * Delete the topic's avatar for the specified topic. + * + *
GitLab Endpoint: PUT /topics/:id
+ * + * @param id the topic in the form of an Integer + * @return the updated Topic instance + * @throws GitLabApiException if any exception occurs + */ + public Topic deleteTopicAvatar(final Integer id) throws GitLabApiException { + Response response = putUpload(Response.Status.OK, "avatar", null, "topics", id); + return (response.readEntity(Topic.class)); + } + + /** + * Delete a topic. You must be an administrator to delete a project topic. When you delete a project topic, you also delete the topic assignment for projects. + * + *
GitLab Endpoint: DELETE /topics/:id
+ * + * @param id the topic to deleted in the form of an Integer + * @throws GitLabApiException if any exception occurs + */ + public void deleteTopic(Integer id) throws GitLabApiException { + if(isApiVersion(GitLabApi.ApiVersion.V3)){ + throw new GitLabApiException("Topics need api v4+"); + } + delete(Response.Status.NO_CONTENT,null, "topics", id); + } + + /** + * Merge two topics together. You must be an administrator to merge a source topic into a target topic. When you merge topics, you delete the source topic and move all assigned projects to the target topic. + * + *
GitLab Endpoint: POST /topics/merge
+ * + * @param sourceTopicId ID of source project topic + * @param targetTopicId ID of target project topic + * @return the merged Topic instance + * @throws GitLabApiException if any exception occurs + */ + public Topic mergeTopics(Integer sourceTopicId, Integer targetTopicId) throws GitLabApiException { + Response response = post(Response.Status.OK,new GitLabApiForm().withParam("source_topic_id",sourceTopicId).withParam("target_topic_id",targetTopicId),"topics/merge"); + return (response.readEntity(Topic.class)); + } +} diff --git a/src/main/java/org/gitlab4j/api/models/Project.java b/src/main/java/org/gitlab4j/api/models/Project.java index 4533ca9c1..55c9f5748 100644 --- a/src/main/java/org/gitlab4j/api/models/Project.java +++ b/src/main/java/org/gitlab4j/api/models/Project.java @@ -80,7 +80,9 @@ public String toString() { private Boolean snippetsEnabled; private String sshUrlToRepo; private Integer starCount; + private List tagList; + private List topics; private Integer visibilityLevel; private Visibility visibility; private Boolean wallEnabled; @@ -542,19 +544,44 @@ public void setStarCount(Integer starCount) { this.starCount = starCount; } + /** + * Tags will be removed in API v5 + */ + @Deprecated public List getTagList() { return tagList; } + /** + * Tags will be removed in API v5 + */ + @Deprecated public void setTagList(List tagList) { this.tagList = tagList; } + /** + * Tags will be removed in API v5 + */ + @Deprecated public Project withTagList(List tagList) { this.tagList = tagList; return (this); } + public List getTopics() { + return topics; + } + + public void setTopics(List topics) { + this.topics = topics; + } + + public Project withTopics(List topics) { + this.topics = topics; + return (this); + } + public Visibility getVisibility() { return visibility; } @@ -733,7 +760,7 @@ public String toString() { * Formats a fully qualified project path based on the provided namespace and project path. * * @param namespace the namespace, either a user name or group name - * @param path the project path + * @param path the project path * @return a fully qualified project path based on the provided namespace and project path */ public static final String getPathWithNammespace(String namespace, String path) { diff --git a/src/main/java/org/gitlab4j/api/models/ProjectFilter.java b/src/main/java/org/gitlab4j/api/models/ProjectFilter.java index a966349f5..748cd5579 100644 --- a/src/main/java/org/gitlab4j/api/models/ProjectFilter.java +++ b/src/main/java/org/gitlab4j/api/models/ProjectFilter.java @@ -35,6 +35,9 @@ public class ProjectFilter { private Date lastActivityAfter; private Date lastActivityBefore; private String repositoryStorage; + private Boolean imported; + private String topic; + private Integer topic_id; /** * Limit by archived status. @@ -305,6 +308,39 @@ public ProjectFilter withRepositoryStorage(String repositoryStorage) { return (this); } + /** + * Limit results to projects which were imported from external systems by current user. + * + * @param imported limit results to projects imported from external systems by current user + * @return the reference to this ProjectFilter instance + */ + public ProjectFilter withImported(Boolean imported){ + this.imported = imported; + return (this); + } + + /** + * Limit results to projects that match all of given topics. + * + * @param topic Comma-separated topic names. + * @return the reference to this ProjectFilter instance + */ + public ProjectFilter withTopic(String topic){ + this.topic = topic; + return (this); + } + + /** + * Limit results to projects with the assigned topic given by the topic ID. + * + * @param topic_id the topic ID + * @return the reference to this ProjectFilter instance + */ + public ProjectFilter withTopicId(Integer topic_id){ + this.topic_id = topic_id; + return (this); + } + /** * Get the query params specified by this filter. * @@ -348,6 +384,9 @@ public GitLabApiForm getQueryParams() { .withParam("last_activity_after", lastActivityAfter) .withParam("last_activity_before", lastActivityBefore) .withParam("repository_storage", repositoryStorage) + .withParam("imported",imported) + .withParam("topic",topic) + .withParam("topic_id",topic_id) ); } } diff --git a/src/main/java/org/gitlab4j/api/models/Topic.java b/src/main/java/org/gitlab4j/api/models/Topic.java new file mode 100644 index 000000000..667723ba6 --- /dev/null +++ b/src/main/java/org/gitlab4j/api/models/Topic.java @@ -0,0 +1,71 @@ +package org.gitlab4j.api.models; + +import org.gitlab4j.api.utils.JacksonJson; + +public class Topic { + + private Integer id; + + private String name; + + private String title; + + private String description; + + private int totalProjectsCount; + + private String avatarUrl; + + public Integer getId() { + return id; + } + + public void setId(Integer id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public int getTotalProjectsCount() { + return totalProjectsCount; + } + + public void setTotalProjectsCount(int totalProjectsCount) { + this.totalProjectsCount = totalProjectsCount; + } + + public String getAvatarUrl() { + return avatarUrl; + } + + public void setAvatarUrl(String avatarUrl) { + this.avatarUrl = avatarUrl; + } + + @Override + public String toString() { + return (JacksonJson.toJsonString(this)); + } +} diff --git a/src/main/java/org/gitlab4j/api/models/TopicParams.java b/src/main/java/org/gitlab4j/api/models/TopicParams.java new file mode 100644 index 000000000..e3513489c --- /dev/null +++ b/src/main/java/org/gitlab4j/api/models/TopicParams.java @@ -0,0 +1,52 @@ +package org.gitlab4j.api.models; + +import org.gitlab4j.api.GitLabApiForm; +import org.gitlab4j.api.TopicsApi; + +import java.io.File; + +/** + * This class is utilized by the {@link TopicsApi#createTopic(TopicParams)} + * and {@link TopicsApi#updateTopic(Integer, TopicParams)} methods to set + * the parameters for the call to the GitLab API. + * + * Avatar Upload has its own Upload in {@link TopicsApi#updateTopicAvatar(Integer,File)} + */ +public class TopicParams { + + private String name; + private String title; + private String description; + + public TopicParams withName(String name) { + this.name = name; + return (this); + } + + public TopicParams withTitle(String title) { + this.title = title; + return (this); + } + + public TopicParams withDescription(String description) { + this.description = description; + return (this); + } + + /** + * Get the form params for a group create oir update call. + * + * @param isCreate set to true for a create group call, false for update + * @return a GitLabApiForm instance holding the parameters for the group create or update operation + * @throws RuntimeException if required parameters are missing + */ + public GitLabApiForm getForm(boolean isCreate) { + + GitLabApiForm form = new GitLabApiForm() + .withParam("name", name, isCreate) + .withParam("title", title, isCreate) + .withParam("description", description); + + return (form); + } +} diff --git a/src/test/java/org/gitlab4j/api/TestGitLabApiBeans.java b/src/test/java/org/gitlab4j/api/TestGitLabApiBeans.java index 7e8de8d16..49f48d135 100644 --- a/src/test/java/org/gitlab4j/api/TestGitLabApiBeans.java +++ b/src/test/java/org/gitlab4j/api/TestGitLabApiBeans.java @@ -117,6 +117,7 @@ import org.gitlab4j.api.models.SystemHook; import org.gitlab4j.api.models.Tag; import org.gitlab4j.api.models.Todo; +import org.gitlab4j.api.models.Topic; import org.gitlab4j.api.models.TreeItem; import org.gitlab4j.api.models.Trigger; import org.gitlab4j.api.models.User; @@ -735,6 +736,12 @@ public void testTodos() throws Exception { assertTrue(compareJson(todos, "todos.json")); } + @Test + public void testTopic() throws Exception { + Topic topic = unmarshalResource(Topic.class, "topic.json"); + assertTrue(compareJson(topic, "topic.json")); + } + @Test public void testTree() throws Exception { List tree = unmarshalResourceList(TreeItem.class, "tree.json"); diff --git a/src/test/java/org/gitlab4j/api/TestTopicsApi.java b/src/test/java/org/gitlab4j/api/TestTopicsApi.java new file mode 100644 index 000000000..6f3d32d3d --- /dev/null +++ b/src/test/java/org/gitlab4j/api/TestTopicsApi.java @@ -0,0 +1,48 @@ +package org.gitlab4j.api; + +import static org.gitlab4j.api.JsonUtils.compareJson; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; +import static org.mockito.MockitoAnnotations.openMocks; + +import java.io.IOException; + +import javax.ws.rs.core.MultivaluedMap; + +import org.gitlab4j.api.models.Topic; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.Mock; +import org.mockito.Mockito; + +public class TestTopicsApi implements Constants { + + @Mock private GitLabApi gitLabApi; + @Mock private GitLabApiClient gitLabApiClient; + @Captor private ArgumentCaptor> attributeCaptor; + private MockResponse response; + + @BeforeEach + public void setUp() throws Exception { + openMocks(this); + } + + @Test + public void testGetTopic() throws Exception { + initGetTopic(); + Topic result = new TopicsApi(gitLabApi).getTopic(1); + assertNotNull(result); + assertTrue(compareJson(result, "topic.json")); + } + + private void initGetTopic() throws Exception, IOException { + response = new MockResponse(Topic.class, "topic.json", null); + when(gitLabApi.getApiClient()).thenReturn(gitLabApiClient); + when(gitLabApiClient.validateSecretToken(any())).thenReturn(true); + when(gitLabApiClient.get(attributeCaptor.capture(), Mockito.any())).thenReturn(response); + } +} diff --git a/src/test/resources/org/gitlab4j/api/topic.json b/src/test/resources/org/gitlab4j/api/topic.json new file mode 100644 index 000000000..2975782f6 --- /dev/null +++ b/src/test/resources/org/gitlab4j/api/topic.json @@ -0,0 +1,8 @@ +{ + "id": 1, + "name": "gitlab", + "title": "GitLab", + "description": "GitLab is an open source end-to-end software development platform with built-in version control, issue tracking, code review, CI/CD, and more.", + "total_projects_count": 1000, + "avatar_url": "http://www.gravatar.com/avatar/a0d477b3ea21970ce6ffcbb817b0b435?s=80&d=identicon" +}