Unverified Commit 69c70968 authored by NadeoWolf's avatar NadeoWolf Committed by GitHub
Browse files

Add TopicsApi (#1012)




---------

Co-authored-by: default avatarJeremie Bresson <jeremie.bresson@unblu.com>
parent 32d00b19
......@@ -97,6 +97,7 @@ public class GitLabApi implements AutoCloseable {
private SystemHooksApi systemHooksApi;
private TagsApi tagsApi;
private TodosApi todosApi;
private TopicsApi topicsApi;
private UserApi userApi;
private WikisApi wikisApi;
private KeysApi keysApi;
......@@ -1670,6 +1671,25 @@ public class GitLabApi implements AutoCloseable {
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.
......
......@@ -41,6 +41,7 @@ import org.glassfish.jersey.client.JerseyClientBuilder;
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 class GitLabApiClient implements AutoCloseable {
*
* @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 @@ public class GitLabApiClient implements AutoCloseable {
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));
}
......
......@@ -1044,6 +1044,10 @@ public class ProjectApi extends AbstractApi implements Constants {
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);
......@@ -1052,6 +1056,10 @@ public class ProjectApi extends AbstractApi implements Constants {
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");
......@@ -1315,6 +1323,10 @@ public class ProjectApi extends AbstractApi implements Constants {
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);
......@@ -1323,6 +1335,10 @@ public class ProjectApi extends AbstractApi implements Constants {
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);
......
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);
}
/**
* <p>Get a list of Topics. </p>
*
* <strong>WARNING:</strong> 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.
*
* <pre><code>GitLab Endpoint: GET /topics</code></pre>
*
* @return the list of topics viewable by the authenticated user
* @throws GitLabApiException if any exception occurs
*/
public List<Topic> getTopics() throws GitLabApiException {
return (getTopics(getDefaultPerPage()).all());
}
/**
* Get a list of topics in the specified page range.
*
* <pre><code>GitLab Endpoint: GET /topics</code></pre>
*
* @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<Topic> getTopics(int page, int perPage) throws GitLabApiException {
Response response = get(Response.Status.OK, getPageQueryParams(page, perPage), "topics");
return (response.readEntity(new GenericType<List<Topic>>() {}));
}
/**
* Get a Pager of topics.
*
* <pre><code>GitLab Endpoint: GET /topics</code></pre>
*
* @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<Topic> getTopics(int itemsPerPage) throws GitLabApiException {
return (new Pager<Topic>(this, Topic.class, itemsPerPage, null, "topics"));
}
/**
* Get a Stream of topics.
*
* <pre><code>GitLab Endpoint: GET /topics</code></pre>
*
* @return the stream of topics
* @throws GitLabApiException if any exception occurs
*/
public Stream<Topic> getTopicsStream() throws GitLabApiException {
return (getTopics(getDefaultPerPage()).stream());
}
/**
* Get all details of a topic.
*
* <pre><code>GitLab Endpoint: GET /topics/:id</code></pre>
*
* @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.
*
* <pre><code>GitLab Endpoint: GET /topics/:id</code></pre>
*
* @param id the topic ID
* @return the Topic for the specified topic id as an Optional instance
*/
public Optional<Topic> 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.
*
* <pre><code>GitLab Endpoint: POST /topics</code></pre>
*
* @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.
*
* <pre><code>GitLab Endpoint: PUT /topics/:id</code></pre>
*
* @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.
*
* <pre><code>GitLab Endpoint: PUT /topics/:id</code></pre>
*
* @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.
*
* <pre><code>GitLab Endpoint: PUT /topics/:id</code></pre>
*
* @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.
*
* <pre><code>GitLab Endpoint: DELETE /topics/:id</code></pre>
*
* @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.
*
* <pre><code>GitLab Endpoint: POST /topics/merge</code></pre>
*
* @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));
}
}
......@@ -80,7 +80,9 @@ public class Project {
private Boolean snippetsEnabled;
private String sshUrlToRepo;
private Integer starCount;
private List<String> tagList;
private List<String> topics;
private Integer visibilityLevel;
private Visibility visibility;
private Boolean wallEnabled;
......@@ -542,19 +544,44 @@ public class Project {
this.starCount = starCount;
}
/**
* Tags will be removed in API v5
*/
@Deprecated
public List<String> getTagList() {
return tagList;
}
/**
* Tags will be removed in API v5
*/
@Deprecated
public void setTagList(List<String> tagList) {
this.tagList = tagList;
}
/**
* Tags will be removed in API v5
*/
@Deprecated
public Project withTagList(List<String> tagList) {
this.tagList = tagList;
return (this);
}
public List<String> getTopics() {
return topics;
}
public void setTopics(List<String> topics) {
this.topics = topics;
}
public Project withTopics(List<String> topics) {
this.topics = topics;
return (this);
}
public Visibility getVisibility() {
return visibility;
}
......@@ -733,7 +760,7 @@ public class Project {
* 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) {
......
......@@ -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 class ProjectFilter {
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 class ProjectFilter {
.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)
);
}
}
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));
}
}
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);
}
}
......@@ -122,6 +122,7 @@ import org.gitlab4j.api.models.SshKey;
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;
......@@ -752,6 +753,12 @@ public class TestGitLabApiBeans {
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<TreeItem> tree = unmarshalResourceList(TreeItem.class, "tree.json");
......
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<MultivaluedMap<String, String>> 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.<Object>any())).thenReturn(response);
}
}
{
"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"
}
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment