JavaRush /Java Blog /Random EN /We are adding the ability to subscribe to a group of arti...

We are adding 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 JavaRush. This corresponds to issue JRTB-5 on GitHub. Let me explain: JavaRush has a section called Articles , and in it there are Groups of Articles. The idea is to receive notifications about a new article from one or more groups through a 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 updates from this group and receive a link to a new publication every time. As part of this task, we need to learn how to use the open API for working with groups in JavaRush. Just at this moment such a thing arrived. Here is a link to a description of the open API .
Friends! Do you want to know immediately when 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? Let's figure it out now

We haven't talked about the swagger yet. For those who don’t know, I’ll explain briefly: this is a place where you can openly look at the API of a server and try to make some requests to it. Typically, a swagger groups possible requests. 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, what http method, and so on). I advise you to read and watch more on this topic, because this is the part of development that almost every one of you will encounter. To figure it out, let’s find out how many groups there are in JavaRush. To do this, expand the group-controller group and select Get request /api/1.0/rest/groups/count . It will return us the number of groups in JavaRush. Let's 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 try this request out, 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 - 3You can also configure the type, filter, and query there (it’s actually interesting here: this will be a text search in a group). But for now, let’s run it without any restrictions and see how many groups there are in JavaRush. To do this, click on Execute. Just below there will be a response (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 select 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? It’s easy: you can go to the website, find the article section, select all the groups and add the appropriate filter there ( https://javarush.com/groups/all?type=COMPANY ). And yes, indeed, there are only 4 of them. Although there are actually 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, there are none yet. Just for fun, see what happens if you set filter = MY in a browser where you are logged into Javarush and not logged in. More about the swagger - in this article on Habré .

Writing a client for 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 exactly what objects will arrive. We will also take objects from the swagger, from the Models section (at the very bottom of the page). Let's create a new package and call it javarushclient 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 JavaRush API:
<dependency>
  <groupId>com.konghq</groupId>
  <artifactId>unirest-java</artifactId>
  <version>${unirest.version}</version>
</dependency>
And put the version in the properties block:
<unirest.version>3.11.01</unirest.version>
Once we have a dependency, we can start adding code. Let's create a client for JavaRushGroupClient groups and an implementation in the JavaRushGroupClientImpl class. But first you need to create DTOs (data transfer objects) - 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 section Models , in which you can count them. This is what GroupDiscussionInfo looks like in the swagger: In the javarushclient package, we will create a dto“Java project from A to Z”: Adding the ability to subscribe to a group of articles.  Part 1 - 7 package , to which we will add, based on data from the swagger, the following classes:
  • MeGroupInfoStatus :

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

  • MeGroupInfo :

    package com.github.javarushcommunity.jrtb.javarushclient.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.javarushcommunity.jrtb.javarushclient.dto;
    
    /**
    * Group Info type;
    */
    public enum GroupInfoType {
       UNKNOWN, CITY, COMPANY, COLLEGE, TECH, SPECIAL, COUNTRY
    }

  • UserDiscussionInfo :

    package com.github.javarushcommunity.jrtb.javarushclient.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.javarushcommunity.jrtb.javarushclient.dto;
    
    /**
    * Group Visibility status.
    */
    public enum GroupVisibilityStatus {
       UNKNOWN, RESTRICTED, PUBLIC, PROTECTED, PRIVATE, DISABLED, DELETED
    }

  • Then - GroupInfo :

    package com.github.javarushcommunity.jrtb.javarushclient.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 completely the same, let’s link them in inheritance - GroupDiscusionInfo :
package com.github.javarushcommunity.jrtb.javarushclient.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 will also need a filter for the GroupFilter request :
package com.github.javarushcommunity.jrtb.javarushclient.dto;

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

   UNKNOWN, MY, ALL
}
In a request to get 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.javarushcommunity.jrtb.javarushclient.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;
   }
}
For searching the number of groups, it is slightly different. It only has query, type and filter. And it would seem that you don’t want to duplicate the code. At the same time, if you start combining them, it turns out ugly when working with builders. So I decided to separate them and repeat the code. This is what GroupCountRequestArgs looks like :
package com.github.javarushcommunity.jrtb.javarushclient.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 didn’t mention that the last two classes have a populateQueries method, which will prepare the map for creating a query (you’ll see it later). Based on the classes described above, let's create an interface for JavaRushGroupClient :
package com.github.javarushcommunity.jrtb.javarushclient;

import com.github.javarushcommunity.jrtb.javarushclient.dto.GroupDiscussionInfo;
import com.github.javarushcommunity.jrtb.javarushclient.dto.GroupInfo;
import com.github.javarushcommunity.jrtb.javarushclient.dto.GroupRequestArgs;
import com.github.javarushcommunity.jrtb.javarushclient.dto.GroupsCountRequestArgs;

import java.util.List;

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

    /**
     * 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 the case when we want to get GroupInfo or GroupDiscussionInfo information added. Otherwise, these requests are identical, and the only difference will be that in one the includeDiscussion flag will be true, and in the other it will be false. Therefore, there were 4 methods, not three. Now let's start implementing:
package com.github.javarushcommunity.jrtb.javarushclient;

import com.github.javarushcommunity.jrtb.javarushclient.dto.GroupDiscussionInfo;
import com.github.javarushcommunity.jrtb.javarushclient.dto.GroupInfo;
import com.github.javarushcommunity.jrtb.javarushclient.dto.GroupRequestArgs;
import com.github.javarushcommunity.jrtb.javarushclient.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 JavaRushGroupClient} interface.
*/
@Component
public class JavaRushGroupClientImpl implements JavaRushGroupClient {

   private final String javarushApiGroupPath;

   public JavaRushGroupClientImpl(@Value("${javarush.api.path}") String javarushApi) {
       this.javarushApiGroupPath = javarushApi + "/groups";
   }

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

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

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

   @Override
   public GroupDiscussionInfo getGroupById(Integer id) {
       return Unirest.get(String.format("%s/group%s", javarushApiGroupPath, 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 properties file. Therefore, let's add a new line to application.properties:
javarush.api.path=https://javarush.com/api/1.0/rest
This value will now be in one place for all API clients, and if the API path changes, we will quickly update it. Previously, I hammered nails with a microscope, received a response from an http request via Unirest, translated it into a string and then parsed this string through Jackson... It was scary, tedious and required many additional things. In this library you can see what it looks like. As soon as I get my hands on it, I’ll refactor everything.
Anyone who would like 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. This will be real work experience for you, but I don’t mind. I will conduct a full code review and help if necessary.
Now the question is: does our code work as we expect? The answer is easy: you just need to write tests for them. As I have said more than once, developers must be able to write tests. Therefore, using our Swagger UI, we will send requests, look at the responses and substitute them into the tests as the expected result. You may have immediately noticed that the number of groups is not static and can change. And you're right. The only question is how often does this number change? Very rarely, so over the course of several months we can say that this value will be static. And if something changes, we will update the tests. Meet - JavaRushGroupClientTest:
package com.github.javarushcommunity.jrtb.javarushclient;

import com.github.javarushcommunity.jrtb.javarushclient.dto.GroupDiscussionInfo;
import com.github.javarushcommunity.jrtb.javarushclient.dto.GroupInfo;
import com.github.javarushcommunity.jrtb.javarushclient.dto.GroupRequestArgs;
import com.github.javarushcommunity.jrtb.javarushclient.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.javarushcommunity.jrtb.javarushclient.dto.GroupInfoType.TECH;

@DisplayName("Integration-level testing for JavaRushGroupClientImplTest")
class JavaRushGroupClientTest {

   private final JavaRushGroupClient groupClient = new JavaRushGroupClientImpl("https://javarush.com/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());
   }
}
The tests are written in the same style as before. There are several tests for each request. There is no point in testing everything, since I think that this API has already been tested in the best way.

Conclusion

As part of this article, we added a Java client for groups to the JavaRush API. As they say, live and learn. While I was writing this client, I took advantage of their documentation and conveniently used the work with objects that they provide. I draw your attention to the task that I proposed. If anyone is interested, write me a private 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 adding command and (if we fit it into one article) we will add getting a list of groups to which the user is subscribed. Next, anyone who has the desire and talent to write texts for the bot, please write to me in a PM. I am not an expert in this matter and any help would be very helpful. Let's formalize all this as open source development, it will be interesting! Well, as usual - like, subscribe, bell , give our project a star , write comments and rate the article!
useful links

A list of all materials in the series is at the beginning of this article.

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