JavaRush /Java Blog /Random EN /We add the ability to subscribe to a group of articles. (...

We add the ability to subscribe to a group of articles. (Part 1) - "Java project from A to Z"

Published in the Random EN group
Hello! Today we will add a subscription to a group of articles in CodeGym. This corresponds to issue JRTB-5 on GitHub. Let me explain: in CodeGym there is a section Articles , and in it - Groups of articles. The idea is to receive notifications about a new article from one or more groups through the telegram bot.“Java project from A to Z”: Adding the ability to subscribe to a group of articles.  Part 1 - 1

Add JRTB-5

Let's say I'm interested in articles from the Success Stories group . Therefore, I want to subscribe to the updates of this group and receive a link to a new publication each time. As part of this task, we need to learn how to use the open API for working with groups in CodeGym. Just at this moment, such a thing arrived in time. Here is a link to a description of the open API .
Friends! Do you want to know immediately when a new code for a project or a new article is released? Join my tg channel . There I collect my articles, thoughts and open-source development together.

What is swagger? Now let's figure it out

We haven't talked about swagger yet. For those who do not know, I will explain briefly: this is a place where you can openly look at the API of some server and try to make some requests to it. Usually, possible requests are grouped in a swagger. In our case, there are three groups: forum-question , group , post. In each group there will be one or more requests indicating all the necessary data to build this request (that is, what additional parameters can be passed, what to do with them, which http method, and so on). I advise you to read and watch more on this topic, because this is the part of the development that almost every one of you will face. To understand, let's find out how many groups there are in CodeGym. To do this, expand the group-controller group and select the Get request /api/1.0/rest/groups/count . It will return us the number of groups in CodeGym. We look: “Java project from A to Z”: Adding the ability to subscribe to a group of articles.  Part 1 - 2The image shows that this query supports several parameters (query, type, filter). To taste this request, you need to find the Try it out button , after which these parameters can be configured:“Java project from A to Z”: Adding the ability to subscribe to a group of articles.  Part 1 - 3Also, there you can configure the type, and the filter, and the check (it's generally interesting here: it will be a search by text in a group). But for now, let's run it without any restrictions and see how many groups there are in CodeGym. To do this, click on Execute. A little lower will be the answer (in the Server Response section) to this request: “Java project from A to Z”: Adding the ability to subscribe to a group of articles.  Part 1 - 4We see that there are 30 groups in total , this request was completed in 95ms and there is a set of some headers in the response. Next, let's try to configure some parameters. Let's choose the type parameter equal to the COMPANY value and see how the result changes: “Java project from A to Z”: Adding the ability to subscribe to a group of articles.  Part 1 - 5There are 4 of them. How to check this? Easy: you can go to the site, find the article section, select all groups and add the appropriate filter there ( https://codegym.cc/groups/all?type=COMPANY ). And yes, indeed, there are only 4 of them. Although there are really three: D“Java project from A to Z”: Adding the ability to subscribe to a group of articles.  Part 1 - 6So far it fits. By the way, if we check the universities, they don't exist yet. For the sake of interest, see what happens if you put filter = MY in a browser where you are logged into Javarush and not logged in. More about swagger - in this article on Habré .

Writing a Client to the Javarush API for Groups

Now, based on the open API, we will write a Java client that can make requests, receive responses, and knows which objects will come. We will also take the objects from the swagger, from the Models section (at the very bottom of the page). Let's create a new package and name it codegymclient next to service, repository. In the future, we will move this into a separate library within the Javarush Community organization and will use it exclusively as a dependency. First of all, you need to add Unitrest - a library for creating http requests to the CodeGym API:
<dependency>
  <groupId>com.konghq</groupId>
  <artifactId>unirest-java</artifactId>
  <version>${unirest.version}</version>
</dependency>
And we move the version to the properties block:
<unirest.version>3.11.01</unirest.version>
When we have a dependency, we can start adding code. Let's create a client for CodeGymGroupClient groups and an implementation in the CodeGymGroupClientImpl class. But first you need to create DTO-shki (data transfer object) - that is, classes whose objects will carry all the data necessary for the client. All models can be viewed in the swagger. At the very bottom there is a Models section where you can read them. Here's what GroupDiscussionInfo looks like in the swagger: “Java project from A to Z”: Adding the ability to subscribe to a group of articles.  Part 1 - 7In the codegymclient package, create the dto package , in which we add the following classes based on the data from the swagger:
  • MeGroupInfoStatus :

    package com.github.codegymcommunity.jrtb.codegymclient.dto;
    
    /**
    * Member group status.
    */
    public enum MeGroupInfoStatus {
       UNKNOWN, CANDIDATE, INVITEE, MEMBER, EDITOR, MODERATOR, ADMINISTRATOR, BANNED
    }

  • GroupInfo :

    package com.github.codegymcommunity.jrtb.codegymclient.dto;
    
    import lombok.Data;
    
    /**
    * Group information related to authorized user. If there is no user - will be null.
    */
    @Data
    public class MeGroupInfo {
       private MeGroupInfoStatus status;
       private Integer userGroupId;
    }

  • GroupInfoType :

    package com.github.codegymcommunity.jrtb.codegymclient.dto;
    
    /**
    * Group Info type;
    */
    public enum GroupInfoType {
       UNKNOWN, CITY, COMPANY, COLLEGE, TECH, SPECIAL, COUNTRY
    }

  • UserDiscussionInfo :

    package com.github.codegymcommunity.jrtb.codegymclient.dto;
    
    import lombok.Data;
    
    /**
    * DTO for User discussion info.
    */
    @Data
    public class UserDiscussionInfo {
    
       private Boolean isBookmarked;
       private Integer lastTime;
       private Integer newCommentsCount;
    }

  • GroupVisibilitysdtatus :

    package com.github.codegymcommunity.jrtb.codegymclient.dto;
    
    /**
    * Group Visibility status.
    */
    public enum GroupVisibilityStatus {
       UNKNOWN, RESTRICTED, PUBLIC, PROTECTED, PRIVATE, DISABLED, DELETED
    }

  • Then - GroupInfo :

    package com.github.codegymcommunity.jrtb.codegymclient.dto;
    
    import lombok.Data;
    import lombok.ToString;
    
    /**
    * Group Info DTO class.
    */
    @Data
    @ToString
    public class GroupInfo {
    
       private Integer id;
       private String avatarUrl;
       private String createTime;
       private String description;
       private String key;
       private Integer levelToEditor;
       private MeGroupInfo meGroupInfo;
       private String pictureUrl;
       private String title;
       private GroupInfoType type;
       private Integer userCount;
       private GroupVisibilityStatus visibilityStatus;
    }

Since GroupInfo and GroupDiscussionInfo are almost identical, let's link them in inheritance - GroupDiscussionInfo :
package com.github.codegymcommunity.jrtb.codegymclient.dto;

import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;

/**
* Group discussion info class.
*/
@EqualsAndHashCode(callSuper = true)
@Data
@ToString(callSuper = true)
public class GroupDiscussionInfo extends GroupInfo {

   private UserDiscussionInfo userDiscussionInfo;
   private Integer commentsCount;
}
We also need a filter for the GroupFilter query :
package com.github.codegymcommunity.jrtb.codegymclient.dto;

/**
* Filters for group requests.
*/
public enum GroupFilter {

   UNKNOWN, MY, ALL
}
In a get request by ID, it returns GroupDiscussionInfo, and in a request for a collection of groups, you can get both GroupInfo and GroupDiscussionInfo. Since requests can have type, query, filter, offset and limit, let's create a separate GroupRequestArgs class and make it a builder class (read what the builder pattern is):
package com.github.codegymcommunity.jrtb.codegymclient.dto;

import lombok.*;

import java.util.HashMap;
import java.util.Map;

import static java.util.Objects.nonNull;

/**
* Request arguments for group requests.
*/
@Builder
@Getter
public class GroupRequestArgs {

   private final String query;
   private final GroupInfoType type;
   private final GroupFilter filter;

   /**
    * specified where to start getting groups
    */
   private final Integer offset;
   /**
    * Limited number of groups.
    */
   private final Integer limit;

   public Map populateQueries() {
       Map queries = new HashMap<>();
       if(nonNull(query)) {
           queries.put("query", query);
       }
       if(nonNull(type)) {
           queries.put("type", type);
       }
       if(nonNull(filter)) {
           queries.put("filter", filter);
       }
       if(nonNull(offset)) {
           queries.put("offset", offset);
       }
       if(nonNull(limit)) {
           queries.put("limit", limit);
       }
       return queries;
   }
}
To search for the number of groups, it is somewhat different. It has only query, type and filter. And it would seem that you do not want to duplicate the code. However, if you start to combine them, it turns out ugly when working with builders. Therefore, I decided to separate them and repeat the code. Here's what GroupCountRequestArgs looks like :
package com.github.codegymcommunity.jrtb.codegymclient.dto;

import lombok.Builder;
import lombok.Getter;

import java.util.HashMap;
import java.util.Map;

import static java.util.Objects.nonNull;

/**
* Request arguments for group count requests.
*/
@Builder
@Getter
public class GroupsCountRequestArgs {
   private final String query;
   private final GroupInfoType type;
   private final GroupFilter filter;

   public Map populateQueries() {
       Map queries = new HashMap<>();
       if (nonNull(query)) {
           queries.put("query", query);
       }
       if (nonNull(type)) {
           queries.put("type", type);
       }
       if (nonNull(filter)) {
           queries.put("filter", filter);
       }
       return queries;
   }
}
Yes, I did not mention that in the last two classes there is a populateQueries method that will prepare a map for creating a query (see it below). Based on the classes described above, let's create an interface for the CodeGymGroupClient :
package com.github.codegymcommunity.jrtb.codegymclient;

import com.github.codegymcommunity.jrtb.codegymclient.dto.GroupDiscussionInfo;
import com.github.codegymcommunity.jrtb.codegymclient.dto.GroupInfo;
import com.github.codegymcommunity.jrtb.codegymclient.dto.GroupRequestArgs;
import com.github.codegymcommunity.jrtb.codegymclient.dto.GroupsCountRequestArgs;

import java.util.List;

/**
 * Client for Javarush Open API corresponds to Groups.
 */
public interface CodeGymGroupClient {

    /**
     * Get all the {@link GroupInfo} filtered by provided {@link GroupRequestArgs}.
     *
     * @param requestArgs provided {@link GroupRequestArgs}.
     * @return the collection of the {@link GroupInfo} objects.
     */
    List<GroupInfo> getGroupList(GroupRequestArgs requestArgs);

    /**
     * Get all the {@link GroupDiscussionInfo} filtered by provided {@link GroupRequestArgs}.
     *
     * @param requestArgs provided {@link GroupRequestArgs}
     * @return the collection of the {@link GroupDiscussionInfo} objects.
     */
    List<GroupDiscussionInfo> getGroupDiscussionList(GroupRequestArgs requestArgs);

    /**
     * Get count of groups filtered by provided {@link GroupRequestArgs}.
     *
     * @param countRequestArgs provided {@link GroupsCountRequestArgs}.
     * @return the count of the groups.
     */
    Integer getGroupCount(GroupsCountRequestArgs countRequestArgs);

    /**
     * Get {@link GroupDiscussionInfo} by provided ID.
     *
     * @param id provided ID.
     * @return {@link GroupDiscussionInfo} object.
     */
    GroupDiscussionInfo getGroupById(Integer id);
}
Two different requests for when we want to get GroupInfo or GroupDiscussionInfo information added. Otherwise, these requests are identical, and the only difference is that the includeDiscussion flag will be true in one, and false in the other. Therefore, 4 methods came out, not three. Now let's start implementing:
package com.github.codegymcommunity.jrtb.codegymclient;

import com.github.codegymcommunity.jrtb.codegymclient.dto.GroupDiscussionInfo;
import com.github.codegymcommunity.jrtb.codegymclient.dto.GroupInfo;
import com.github.codegymcommunity.jrtb.codegymclient.dto.GroupRequestArgs;
import com.github.codegymcommunity.jrtb.codegymclient.dto.GroupsCountRequestArgs;
import kong.unirest.GenericType;
import kong.unirest.Unirest;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import java.util.List;

/**
* Implementation of the {@link CodeGymGroupClient} interface.
*/
@Component
public class CodeGymGroupClientImpl implements CodeGymGroupClient {

   private final String codegymApiGroupPath;

   public CodeGymGroupClientImpl(@Value("${codegym.api.path}") String codegymApi) {
       this.codegymApiGroupPath = codegymApi + "/groups";
   }

   @Override
   public List<GroupInfo> getGroupList(GroupRequestArgs requestArgs) {
       return Unirest.get(codegymApiGroupPath)
               .queryString(requestArgs.populateQueries())
               .asObject(new GenericType<list<GroupInfo>>() {
               })
               .getBody();
   }

   @Override
   public List<GroupDiscussionInfo> getGroupDiscussionList(GroupRequestArgs requestArgs) {
       return Unirest.get(codegymApiGroupPath)
               .queryString(requestArgs.populateQueries())
               .asObject(new GenericType<list<GroupDiscussionInfo>>() {
               })
               .getBody();
   }

   @Override
   public Integer getGroupCount(GroupsCountRequestArgs countRequestArgs) {
       return Integer.valueOf(
               Unirest.get(String.format("%s/count", codegymApiGroupPath))
                       .queryString(countRequestArgs.populateQueries())
                       .asString()
                       .getBody()
       );
   }

   @Override
   public GroupDiscussionInfo getGroupById(Integer id) {
       return Unirest.get(String.format("%s/group%s", codegymApiGroupPath, id.toString()))
               .asObject(GroupDiscussionInfo.class)
               .getBody();
   }


}
I add the path to the API in the constructor using the already familiar Value annotation. It implies that the value inside the annotation corresponds to a field in the property file. So let's add a new line to application.properties:
codegym.api.path=https://codegym.com/api/1.0/rest
This value will now be in the same place for all API clients, and if the API path changes, we will quickly update it. I used to hammer nails with a microscope, get a response from an http request through Unirest, translate it into a string, and then parse this string through Jackson ... It was scary, dreary and required many additional things. In this library you can see how it looks. As soon as I get my hands on it, I'll refactor everything.
Whoever wants to try to update this library - add receiving objects only using the tools of the unirest library - write in a personal message or as a new issue in the library itself. For you, this will be a real work experience, but I do not mind. I will conduct a full code review and help if necessary.
Now the question is: does our code work the way we expect it to? The answer is simple: you just need to write tests for them. As I have said more than once, developers should be able to write tests. Therefore, using our Swagger UI, we will send requests, look at responses and substitute them into tests as the expected result. You could immediately notice that the number of groups is not static and can change. And you are right. The only question is, how often does this number change? It is very rare, so that in the context of several months it can be argued that this value will be static. And if something changes, we will update the tests. Meet CodeGymGroupClientTest:
package com.github.codegymcommunity.jrtb.codegymclient;

import com.github.codegymcommunity.jrtb.codegymclient.dto.GroupDiscussionInfo;
import com.github.codegymcommunity.jrtb.codegymclient.dto.GroupInfo;
import com.github.codegymcommunity.jrtb.codegymclient.dto.GroupRequestArgs;
import com.github.codegymcommunity.jrtb.codegymclient.dto.GroupsCountRequestArgs;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;

import java.util.List;

import static com.github.codegymcommunity.jrtb.codegymclient.dto.GroupInfoType.TECH;

@DisplayName("Integration-level testing for CodeGymGroupClientImplTest")
class CodeGymGroupClientTest {

   private final CodeGymGroupClient groupClient = new CodeGymGroupClientImpl("https://codegym.cc/api/1.0/rest");

   @Test
   public void shouldProperlyGetGroupsWithEmptyArgs() {
       //given
       GroupRequestArgs args = GroupRequestArgs.builder().build();

       //when
       List<GroupInfo> groupList = groupClient.getGroupList(args);

       //then
       Assertions.assertNotNull(groupList);
       Assertions.assertFalse(groupList.isEmpty());
   }

   @Test
   public void shouldProperlyGetWithOffSetAndLimit() {
       //given
       GroupRequestArgs args = GroupRequestArgs.builder()
               .offset(1)
               .limit(3)
               .build();

       //when
       List<GroupInfo> groupList = groupClient.getGroupList(args);

       //then
       Assertions.assertNotNull(groupList);
       Assertions.assertEquals(3, groupList.size());
   }

   @Test
   public void shouldProperlyGetGroupsDiscWithEmptyArgs() {
       //given
       GroupRequestArgs args = GroupRequestArgs.builder().build();

       //when
       List<GroupDiscussionInfo> groupList = groupClient.getGroupDiscussionList(args);

       //then
       Assertions.assertNotNull(groupList);
       Assertions.assertFalse(groupList.isEmpty());
   }

   @Test
   public void shouldProperlyGetGroupDiscWithOffSetAndLimit() {
       //given
       GroupRequestArgs args = GroupRequestArgs.builder()
               .offset(1)
               .limit(3)
               .build();

       //when
       List<GroupDiscussionInfo> groupList = groupClient.getGroupDiscussionList(args);

       //then
       Assertions.assertNotNull(groupList);
       Assertions.assertEquals(3, groupList.size());
   }

   @Test
   public void shouldProperlyGetGroupCount() {
       //given
       GroupsCountRequestArgs args = GroupsCountRequestArgs.builder().build();

       //when
       Integer groupCount = groupClient.getGroupCount(args);

       //then
       Assertions.assertEquals(30, groupCount);
   }

   @Test
   public void shouldProperlyGetGroupTECHCount() {
       //given
       GroupsCountRequestArgs args = GroupsCountRequestArgs.builder()
               .type(TECH)
               .build();

       //when
       Integer groupCount = groupClient.getGroupCount(args);

       //then
       Assertions.assertEquals(7, groupCount);
   }

   @Test
   public void shouldProperlyGetGroupById() {
       //given
       Integer androidGroupId = 16;

       //when
       GroupDiscussionInfo groupById = groupClient.getGroupById(androidGroupId);

       //then
       Assertions.assertNotNull(groupById);
       Assertions.assertEquals(16, groupById.getId());
       Assertions.assertEquals(TECH, groupById.getType());
       Assertions.assertEquals("android", groupById.getKey());
   }
}
Tests are written in the same style as before. There are several tests for each request. There is no point in testing everything, because I think that this API is already tested in the best way.

Conclusion

As part of this article, we have added a Java client for groups to the CodeGym API. As the saying goes, live and learn. While writing this client, I took advantage of their documentation and conveniently used the work with objects that they provide. I draw attention to the problem that I proposed. If you are interested, write a personal message, I am more than sure that it will be a very interesting experience. This was the first part. In the second, we will directly implement the add command and (if we fit into one article) we will add getting a list of groups to which the user is subscribed. Further, whoever has the desire and talent for writing texts for the bot, please write to me in the PM. I am not an expert in this matter and any help would be very helpful. Let's arrange it all as an open source development, it will be interesting! Well, as usual -like, subscribe, bell , put a star to our project , write comments and rate the article!
useful links
“Java project from A to Z”: Adding the ability to subscribe to a group of articles.  Part 1 - 8

List of all materials in the series at the beginning of this article.

Comments
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION