From 30f18d6c6c195b59d76ba9ffcc2cfeab27fc468c Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?J=C3=A9r=C3=A9mie=20Bresson?= <jeremie.bresson@unblu.com>
Date: Sat, 11 Nov 2023 06:40:23 +0100
Subject: [PATCH] Add EpicFilter (#1005)

Fixes #986
---
 src/main/java/org/gitlab4j/api/EpicsApi.java  |  70 +++--
 .../java/org/gitlab4j/api/GitLabApiForm.java  |   3 +-
 .../org/gitlab4j/api/models/EpicFilter.java   | 293 ++++++++++++++++++
 3 files changed, 343 insertions(+), 23 deletions(-)
 create mode 100644 src/main/java/org/gitlab4j/api/models/EpicFilter.java

diff --git a/src/main/java/org/gitlab4j/api/EpicsApi.java b/src/main/java/org/gitlab4j/api/EpicsApi.java
index 0366932e..42a5c3c2 100644
--- a/src/main/java/org/gitlab4j/api/EpicsApi.java
+++ b/src/main/java/org/gitlab4j/api/EpicsApi.java
@@ -12,6 +12,7 @@ import javax.ws.rs.core.Response;
 import org.gitlab4j.api.models.ChildEpic;
 import org.gitlab4j.api.models.CreatedChildEpic;
 import org.gitlab4j.api.models.Epic;
+import org.gitlab4j.api.models.EpicFilter;
 import org.gitlab4j.api.models.EpicIssue;
 import org.gitlab4j.api.models.EpicIssueLink;
 import org.gitlab4j.api.models.LinkType;
@@ -54,7 +55,7 @@ public class EpicsApi extends AbstractApi {
      *
      * @param groupIdOrPath the group ID, path of the group, or a Group instance holding the group ID or path
      * @param page the page to get
-     * @param perPage the number of issues per page
+     * @param perPage the number of epics per page
      * @return a list of all epics of the requested group and its subgroups in the specified range
      * @throws GitLabApiException if any exception occurs
      */
@@ -69,7 +70,7 @@ public class EpicsApi extends AbstractApi {
      * <pre><code>GitLab Endpoint: GET /groups/:id/epics</code></pre>
      *
      * @param groupIdOrPath the group ID, path of the group, or a Group instance holding the group ID or path
-     * @param itemsPerPage the number of issues per page
+     * @param itemsPerPage the number of epics per page
      * @return the Pager of all epics of the requested group and its subgroups
      * @throws GitLabApiException if any exception occurs
      */
@@ -123,20 +124,28 @@ public class EpicsApi extends AbstractApi {
      * @param sortOrder return epics sorted in ASC or DESC order. Default is DESC
      * @param search search epics against their title and description
      * @param page the page to get
-     * @param perPage the number of issues per page
+     * @param perPage the number of epics per page
      * @return a list of matching epics of the requested group and its subgroups in the specified range
      * @throws GitLabApiException if any exception occurs
      */
     public List<Epic> getEpics(Object groupIdOrPath, Long authorId, String labels,
             EpicOrderBy orderBy, SortOrder sortOrder, String search, int page, int perPage) throws GitLabApiException {
-        GitLabApiForm formData = new GitLabApiForm(page, perPage)
-                .withParam("author_id", authorId)
-                .withParam("labels", labels)
-                .withParam("order_by", orderBy)
-                .withParam("sort", sortOrder)
-                .withParam("search", search);
-        Response response = get(Response.Status.OK, formData.asMap(), "groups", getGroupIdOrPath(groupIdOrPath), "epics");
-        return (response.readEntity(new GenericType<List<Epic>>() { }));
+        EpicFilter filter = createEpicFilter(authorId, labels, orderBy, sortOrder, search);
+        return getEpics(groupIdOrPath, filter);
+    }
+
+    /**
+     * Gets all epics of the requested group and its subgroups using the specified page and per page setting.
+     *
+     * <pre><code>GitLab Endpoint: GET /groups/:id/epics</code></pre>
+     *
+     * @param groupIdOrPath the group ID, path of the group, or a Group instance holding the group ID or path
+     * @param filter epic filter
+     * @return a list of matching epics of the requested group and its subgroups in the specified range
+     * @throws GitLabApiException if any exception occurs
+     */
+    public List<Epic> getEpics(Object groupIdOrPath, EpicFilter filter) throws GitLabApiException {
+        return getEpics(groupIdOrPath, getDefaultPerPage(), filter).all();
     }
 
     /**
@@ -148,7 +157,7 @@ public class EpicsApi extends AbstractApi {
      * @param authorId returns epics created by the given user id
      * @param labels return epics matching a comma separated list of labels names.
      *        Label names from the epic group or a parent group can be used
-     * @param itemsPerPage the number of issues per page
+     * @param itemsPerPage the number of epics per page
      * @param orderBy return epics ordered by CREATED_AT or UPDATED_AT. Default is CREATED_AT
      * @param sortOrder return epics sorted in ASC or DESC order. Default is DESC
      * @param search search epics against their title and description
@@ -157,13 +166,32 @@ public class EpicsApi extends AbstractApi {
      */
     public Pager<Epic> getEpics(Object groupIdOrPath, Long authorId, String labels,
             EpicOrderBy orderBy, SortOrder sortOrder, String search, int itemsPerPage) throws GitLabApiException {
-        GitLabApiForm formData = new GitLabApiForm()
-                .withParam("author_id", authorId)
-                .withParam("labels", labels)
-                .withParam("order_by", orderBy)
-                .withParam("sort", sortOrder)
-                .withParam("search", search);
-        return (new Pager<Epic>(this, Epic.class, itemsPerPage, formData.asMap(), "groups", getGroupIdOrPath(groupIdOrPath), "epics"));
+        EpicFilter filter = createEpicFilter(authorId, labels, orderBy, sortOrder, search);
+        return getEpics(groupIdOrPath, itemsPerPage, filter);
+    }
+
+    /**
+     * Gets all epics of the requested group and its subgroups using the specified page and per page setting.
+     *
+     * <pre><code>GitLab Endpoint: GET /groups/:id/epics</code></pre>
+     *
+     * @param groupIdOrPath the group ID, path of the group, or a Group instance holding the group ID or path
+     * @param filter epic filter
+     * @param itemsPerPage the number of epics per page
+     * @return a list of matching epics of the requested group and its subgroups in the specified range
+     * @throws GitLabApiException if any exception occurs
+     */
+    public Pager<Epic> getEpics(Object groupIdOrPath, int itemsPerPage, EpicFilter filter) throws GitLabApiException {
+        return (new Pager<Epic>(this, Epic.class, itemsPerPage, filter.getQueryParams().asMap(), "groups", getGroupIdOrPath(groupIdOrPath), "epics"));
+    }
+
+    private EpicFilter createEpicFilter(Long authorId, String labels, EpicOrderBy orderBy, SortOrder sortOrder, String search) {
+        return new EpicFilter()
+            .withAuthorId(authorId)
+            .withLabels(labels)
+            .withOrderBy(orderBy)
+            .withSortOrder(sortOrder)
+            .withSearch(search);
     }
 
     /**
@@ -369,7 +397,7 @@ public class EpicsApi extends AbstractApi {
      * @param groupIdOrPath the group ID, path of the group, or a Group instance holding the group ID or path
      * @param epicIid the IID of the epic to get issues for
      * @param page the page to get
-     * @param perPage the number of issues per page
+     * @param perPage the number of epics per page
      * @return a list of all issues belonging to the specified epic in the specified range
      * @throws GitLabApiException if any exception occurs
      */
@@ -385,7 +413,7 @@ public class EpicsApi extends AbstractApi {
      *
      * @param groupIdOrPath the group ID, path of the group, or a Group instance holding the group ID or path
      * @param epicIid the IID of the epic to get issues for
-     * @param itemsPerPage the number of issues per page
+     * @param itemsPerPage the number of epics per page
      * @return the Pager of all issues belonging to the specified epic
      * @throws GitLabApiException if any exception occurs
      */
diff --git a/src/main/java/org/gitlab4j/api/GitLabApiForm.java b/src/main/java/org/gitlab4j/api/GitLabApiForm.java
index b9c09e60..f58deeb1 100644
--- a/src/main/java/org/gitlab4j/api/GitLabApiForm.java
+++ b/src/main/java/org/gitlab4j/api/GitLabApiForm.java
@@ -159,8 +159,7 @@ public class GitLabApiForm extends Form {
         for (Entry<String, ?> variable : variables.entrySet()) {
             Object value = variable.getValue();
             if (value != null) {
-                this.param(name + "[][key]", variable.getKey());
-                this.param(name + "[][value]", value.toString());
+                this.param(name + "[" + variable.getKey() + "]", value.toString());
             }
         }
 
diff --git a/src/main/java/org/gitlab4j/api/models/EpicFilter.java b/src/main/java/org/gitlab4j/api/models/EpicFilter.java
new file mode 100644
index 00000000..a30bf0bd
--- /dev/null
+++ b/src/main/java/org/gitlab4j/api/models/EpicFilter.java
@@ -0,0 +1,293 @@
+package org.gitlab4j.api.models;
+
+import org.gitlab4j.api.Constants.EpicOrderBy;
+import org.gitlab4j.api.Constants.SortOrder;
+
+import java.util.Date;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+import org.gitlab4j.api.GitLabApiForm;
+import org.gitlab4j.api.models.AbstractEpic.EpicState;
+import org.gitlab4j.api.utils.ISO8601;
+import org.gitlab4j.api.utils.JacksonJsonEnumHelper;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonValue;
+
+/**
+ *  This class is used to filter Groups when getting lists of epics.
+ */
+public class EpicFilter {
+
+    private Long authorId;
+    private String authorUsername;
+    private String labels;
+    private EpicOrderBy orderBy;
+    private SortOrder sort;
+    private String search;
+    private EpicState state;
+    private Date createdAfter;
+    private Date updatedAfter;
+    private Date updatedBefore;
+    private Boolean includeAncestorGroups;
+    private Boolean includeDescendantGroups;
+    private String myReactionEmoji;
+    private Map<EpicField, Object> not;
+
+    public enum EpicField {
+
+        AUTHOR_ID, AUTHOR_USERNAME, LABELS;
+
+        private static JacksonJsonEnumHelper<EpicField> enumHelper = new JacksonJsonEnumHelper<>(EpicField.class);
+
+        @JsonCreator
+        public static EpicField forValue(String value) {
+            return enumHelper.forValue(value);
+        }
+
+        @JsonValue
+        public String toValue() {
+            return (enumHelper.toString(this));
+        }
+
+        @Override
+        public String toString() {
+            return (enumHelper.toString(this));
+        }
+    }
+
+    /**
+     * Add 'author id' filter.
+     *
+     * @param authorId the author id filter
+     * @return the reference to this EpicFilter instance
+     */
+    public EpicFilter withAuthorId(Long authorId) {
+        this.authorId = authorId;
+        return (this);
+    }
+
+    /**
+     * Add 'author username' filter.
+     *
+     * @param authorUsername the 'author username' filter
+     * @return the reference to this EpicFilter instance
+     */
+    public EpicFilter withAuthorUsername(String authorUsername) {
+        this.authorUsername = authorUsername;
+        return (this);
+    }
+    
+    /**
+     * Add 'labels' filter.
+     *
+     * @param labels the labels filter
+     * @return the reference to this EpicFilter instance
+     */
+    public EpicFilter withLabels(String labels) {
+        this.labels = labels;
+        return (this);
+    }
+
+    /**
+     * Add 'order by' filter.
+     *
+     * @param orderBy the 'order by' filter
+     * @return the reference to this GroupFilter instance
+     */
+    public EpicFilter withOrderBy(EpicOrderBy orderBy) {
+        this.orderBy = orderBy;
+        return (this);
+    }
+
+    /**
+     * Add 'sort' filter.
+     *
+     * @param sort sort direction, ASC or DESC
+     * @return the reference to this GroupFilter instance
+     */
+    public EpicFilter withSortOrder(SortOrder sort) {
+        this.sort = sort;
+        return (this);
+    }
+
+    /**
+     * Add 'search' filter.
+     *
+     * @param search the 'search' filter
+     * @return the reference to this EpicFilter instance
+     */
+    public EpicFilter withSearch(String search) {
+        this.search = search;
+        return (this);
+    }
+
+    /**
+     * Add 'state' filter.
+     *
+     * @param state the 'state' filter
+     * @return the reference to this EpicFilter instance
+     */
+    public EpicFilter withState(EpicState state) {
+        this.state = state;
+        return (this);
+    }
+
+    /**
+     * Add 'created after' filter.
+     *
+     * @param createdAfter the 'created after' filter
+     * @return the reference to this EpicFilter instance
+     */
+    public EpicFilter withCreatedAfter(Date createdAfter) {
+        this.createdAfter = createdAfter;
+        return (this);
+    }
+
+    /**
+     * Add 'updated after' filter.
+     *
+     * @param updatedAfter the 'updated after' filter
+     * @return the reference to this EpicFilter instance
+     */
+    public EpicFilter withUpdatedAfter(Date updatedAfter) {
+        this.updatedAfter = updatedAfter;
+        return (this);
+    }
+
+    /**
+     * Add 'updated before' filter.
+     *
+     * @param updatedBefore the 'updated before' filter
+     * @return the reference to this EpicFilter instance
+     */
+    public EpicFilter withUpdatedBefore(Date updatedBefore) {
+        this.updatedBefore = updatedBefore;
+        return (this);
+    }
+
+    /**
+     * Add 'include ancestor groups' filter.
+     *
+     * @param includeAncestorGroups the 'include ancestor groups' filter
+     * @return the reference to this EpicFilter instance
+     */
+    public EpicFilter withIncludeAncestorGroups(Boolean includeAncestorGroups) {
+        this.includeAncestorGroups = includeAncestorGroups;
+        return (this);
+    }
+
+    /**
+     * Add 'include descendant groups' filter.
+     *
+     * @param includeDescendantGroups the 'include descendant groups' filter
+     * @return the reference to this EpicFilter instance
+     */
+    public EpicFilter withIncludeDescendantGroups(Boolean includeDescendantGroups) {
+        this.includeDescendantGroups = includeDescendantGroups;
+        return (this);
+    }
+
+    /**
+     * Add 'my reaction emoji' filter.
+     *
+     * @param myReactionEmoji the 'my reaction emoji' filter
+     * @return the reference to this EpicFilter instance
+     */
+    public EpicFilter withMyReactionEmoji(String myReactionEmoji) {
+        this.myReactionEmoji = myReactionEmoji;
+        return (this);
+    }
+
+    /**
+     * Add 'not' filter.
+     *
+     * @param not the 'not' filter
+     * @return the reference to this EpicFilter instance
+     */
+    public EpicFilter withNot(Map<EpicField, Object> not) {
+        this.not = not;
+        return (this);
+    }
+
+    /**
+     * Add author_id to the 'not' filter entry.
+     *
+     * @param authorId the id of the author to add to the filter
+     * @return the reference to this EpicFilter instance
+     */
+    public EpicFilter withoutAuthorId(Long authorId) {
+        return withNot(EpicField.AUTHOR_ID, authorId);
+    }
+
+    /**
+     * Add author_username to the 'not' filter entry.
+     *
+     * @param authorUsername the username of the author to add to the filter
+     * @return the reference to this EpicFilter instance
+     */
+    public EpicFilter withoutAuthorUsername(String authorUsername) {
+        return withNot(EpicField.AUTHOR_USERNAME, authorUsername);
+    }
+
+    /**
+     * Add labels to the 'not' filter entry.
+     *
+     * @param labels the labels to add to the filter
+     * @return the reference to this EpicFilter instance
+     */
+    public EpicFilter withoutLabels(String... labels) {
+        return withNot(EpicField.LABELS, String.join(",", labels));
+    }
+
+    /**
+     * Add 'not' filter entry.
+     *
+     * @param field the field to be added to the 'not' value
+     * @param value the value for the entry
+     * @return the reference to this EpicFilter instance
+     */
+    public EpicFilter withNot(EpicField field, Object value) {
+        if(not == null) {
+            not = new LinkedHashMap<>();
+        }
+        not.put(field, value);
+        return (this);
+    }
+
+    /**
+     * Get the query params specified by this filter.
+     *
+     * @return a GitLabApiForm instance holding the query parameters for this GroupFilter instance
+     */
+    public GitLabApiForm getQueryParams() {
+        return (new GitLabApiForm()
+            .withParam("author_id", authorId)
+            .withParam("author_username", authorUsername)
+            .withParam("labels", labels)
+            .withParam("order_by", orderBy)
+            .withParam("sort", sort)
+            .withParam("search", search)
+            .withParam("state", state)
+            .withParam("created_after", ISO8601.toString(createdAfter, false))
+            .withParam("updated_after", ISO8601.toString(updatedAfter, false))
+            .withParam("updated_before", ISO8601.toString(updatedBefore, false))
+            .withParam("include_ancestor_groups", includeAncestorGroups)
+            .withParam("include_descendant_groups", includeDescendantGroups)
+            .withParam("my_reaction_emoji", myReactionEmoji)
+            .withParam("not", toStringMap(not), false)
+        );
+    }
+
+    private Map<String, Object> toStringMap(Map<EpicField, Object> map) {
+        if(map == null) {
+            return null;
+        }
+        Map<String, Object> result = new LinkedHashMap<>();
+        for (Map.Entry<EpicField, Object> entry : map.entrySet()) {
+            result.put(entry.getKey().toString(), entry.getValue());
+        }
+        return result;
+    }
+}
-- 
GitLab