Showing posts with label rest. Show all posts
Showing posts with label rest. Show all posts

Tuesday, March 26, 2019

Spring Boot Data JPA (with REST)

0. Intro 

It's getting more and more easier to create rest services with spring boot : now It's possible to create them with just several annotations! Let's see :)

1. Project structure

I created simple gradle project with some tests:



2.  Dependencies (build.gradle)

We need:
- spring-data-jpa   - to use auto-generated by Spring repositories
- sparing-data-rest - to expose these repositories by REST services
- lombok - to reduce code for entities classes
- h2 - as database

plugins {
   id 'org.springframework.boot' version '2.1.3.RELEASE'   id 'java'}

apply plugin: 'io.spring.dependency-management'
group = 'com.demien'version = '0.0.1-SNAPSHOT'sourceCompatibility = '1.8'
repositories {
   mavenCentral()
}

dependencies {
   implementation 'org.springframework.boot:spring-boot-starter-data-jpa'   implementation 'org.springframework.boot:spring-boot-starter-data-rest'   runtimeOnly 'com.h2database:h2'   compileOnly('org.projectlombok:lombok')
   testImplementation 'org.springframework.boot:spring-boot-starter-test'}


3. Main runner class 

Nothing interesting is here - just spring boot runner:

package com.demien.sprdata;
import org.springframework.boot.SpringApplication;import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplicationpublic class SprdataApplication {

   public static void main(String[] args) {
      final SpringApplication app = new SpringApplication(SprdataApplication.class);      app.run(args);   }

}



4. Domain entities 

I create only 2 entities: "parent" : UserGroup and "child": User.

4.1. UserGroup entity 

Thanks to lombok, it's very simple, we just have to list properties. Also we have to annotate it as "@Entity" and also I'm defining named query "UserGroup.namedQueryByName"| :

package com.demien.sprdata.domain;
import javax.persistence.Entity;import javax.persistence.GeneratedValue;import javax.persistence.GenerationType;import javax.persistence.Id;import javax.persistence.NamedQuery;
import lombok.Getter;import lombok.Setter;
@Entity@Getter@Setterpublic class UserGroup {

   @Id   @GeneratedValue(strategy = GenerationType.AUTO)
   private Long id;   private String name;   private String description;
   public UserGroup() {

   }

}



4.2. User entity

It's little bit more complicated: we have to define relationship with parent: we will be joining by field groupId. And I'm also defining named query.

package com.demien.sprdata.domain;
import javax.persistence.Entity;import javax.persistence.FetchType;import javax.persistence.GeneratedValue;import javax.persistence.GenerationType;import javax.persistence.Id;import javax.persistence.JoinColumn;import javax.persistence.ManyToOne;import javax.persistence.NamedQuery;
import lombok.Getter;import lombok.Setter;
@Getter@Setter@Entity@NamedQuery(name = "User.namedQueryByName", query = "SELECT u FROM User u WHERE u.name = :name ")
public class User {

   @Id   @GeneratedValue(strategy = GenerationType.AUTO)
   private Long id;   @ManyToOne(fetch = FetchType.LAZY)
   @JoinColumn(name = "groupId")
   private UserGroup group;   private String name;   private Integer salary;
   public User() {
   }

}


5. Repositories

The magic is happening here. We just have to define interface ..... and that's it! We don't have to create the implementation - it will be created by Spring!!!

5.1. UserGroup repository 

I made it very simple: we're just extending CrudRepository and adding several methods.
"Crud" means Create, Update and Delete - so all these methods will be available in implementation which will be generated by Spring. And also 2 more methods added: findAll and count;

package com.demien.sprdata.repository;
import org.springframework.data.repository.CrudRepository;
import com.demien.sprdata.domain.UserGroup;
public interface UserGroupRepository extends CrudRepository<UserGroup, Long> {

   Iterable<UserGroup> findAll();
   long count();
}


5.2. UserRepository 

It's more complicated. First of all, we're annotation it with @RepositoryRestResource to expose methods as rest services.  Next - I'm using PagingAndSorting repository, so paging and sorting features will be available. Also I'm adding a lot of methods which will be generated by Spring:
- we can use patter [find | count] By [fieldName].
- we can even use fields of parent entity (UserGroup which is accessed by "group" field in User entity)
- we can define our own queries


package com.demien.sprdata.repository;
import java.util.List;
import org.springframework.data.jpa.repository.Query;import org.springframework.data.repository.PagingAndSortingRepository;import org.springframework.data.repository.query.Param;import org.springframework.data.rest.core.annotation.RepositoryRestResource;
import com.demien.sprdata.domain.User;
//http://localhost:8080/users/@RepositoryRestResource(collectionResourceRel = "users", path = "users")
public interface UserRepository extends PagingAndSortingRepository<User, Long> {
   Iterable<User> findAll();
   long count();
   List<User> findByName(String name);
   Long deleteByName(String name);
   Long countByGroupName(String groupName);
   // find by parent entity : Group   List<User> findByGroupName(String name);
   // defining custom query   @Query("SELECT u FROM User u WHERE u.name LIKE CONCAT('%', :name, '%') ")
   List<User> queryByName(@Param("name") String name);
   // using named query defined in entity class   List<User> namedQueryByName(@Param("name") String name);
   @Query(value = "SELECT * FROM User WHERE name = :name ", nativeQuery = true)
   List<User> nativeQueryByName(@Param("name") String name);
}

6. Testing 

And now let's test how this magic works

6.1 UserGroupRepository -  test

Here I'm testing just CRUD operations:


package com.demien.sprdata;
import static org.junit.Assert.assertEquals;import static org.junit.Assert.assertFalse;import static org.junit.Assert.assertTrue;
import java.util.ArrayList;import java.util.List;import java.util.Optional;
import org.junit.Test;import org.junit.runner.RunWith;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;import org.springframework.boot.test.autoconfigure.orm.jpa.TestEntityManager;import org.springframework.test.context.junit4.SpringRunner;
import com.demien.sprdata.domain.UserGroup;import com.demien.sprdata.repository.UserGroupRepository;
@DataJpaTest@RunWith(SpringRunner.class)
public class UserGroupRepositoryTest {

   @Autowired   private UserGroupRepository groupRepository;
   @Autowired   private TestEntityManager em;
   @Test   public void findAllTest() {
      final List<UserGroup> groups = new ArrayList<>();      groupRepository.findAll().forEach(group -> groups.add(group));      assertEquals(4, groups.size());   }

   @Test   public void checkUserGroupCount() {
      assertEquals(4, groupRepository.count());
   }

   @Test   public void findOne() {
      Optional<UserGroup> opGroup = groupRepository.findById(1001L);      assertTrue(opGroup.isPresent());      assertEquals("ADM", opGroup.get().getName());
      groupRepository.deleteById(1001L);      opGroup = groupRepository.findById(1001L);      assertFalse(opGroup.isPresent());
   }

   @Test   public void createNewTest() {
      final UserGroup newGroup = new UserGroup();      newGroup.setDescription("Created");      groupRepository.save(newGroup);      assertTrue(newGroup.getId() != null);
      em.flush();      final Optional<UserGroup> loaded = groupRepository.findById(newGroup.getId());      assertTrue(loaded.isPresent());      assertTrue(loaded.get().getDescription().equals("Created"));
      groupRepository.deleteById(newGroup.getId());
   }

}



6.2. UserRepository - test

And most interesting is happening here: I'm testing paging, sorting, named queries, native queries....

package com.demien.sprdata;
import static org.junit.Assert.assertTrue;
import java.util.List;
import org.junit.Test;import org.junit.runner.RunWith;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;import org.springframework.boot.test.autoconfigure.orm.jpa.TestEntityManager;import org.springframework.data.domain.Page;import org.springframework.data.domain.PageRequest;import org.springframework.data.domain.Sort;import org.springframework.test.context.junit4.SpringRunner;
import com.demien.sprdata.domain.User;import com.demien.sprdata.repository.UserRepository;
@DataJpaTest@RunWith(SpringRunner.class)
public class UserRepositoryTest {

   @Autowired   UserRepository userRepository;
   @Autowired   TestEntityManager em;
   @Test   public void sortTest() {
      Sort sort = new Sort(Sort.Direction.ASC, "group_id").and(new Sort(Sort.Direction.DESC, "name"));      Iterable<User> users = userRepository.findAll(sort);      User first = users.iterator().next();      assertTrue(first.getGroup().getId() == 1001L);      assertTrue(first.getName().equals("Victor"));   }

   @Test   public void pagingTest() {
      final PageRequest pageRequest = PageRequest.of(0, 2);      final Page<User> userPage = userRepository.findAll(pageRequest);      assertTrue(userPage.getNumberOfElements() == 2);      assertTrue(userPage.getTotalPages() == 4);
   }

   @Test   public void findTest() {
      List<User> users = userRepository.findByName("Joe");      assertTrue(users.size() == 1);      assertTrue(users.get(0).getName().equals("Joe"));
      Long countByGroupName = userRepository.countByGroupName("ADM");      assertTrue(countByGroupName == 3L);
      users = userRepository.findByGroupName("ADM");      assertTrue(users.size() == 3);
      users = userRepository.queryByName("a");      assertTrue(users.size() == 5);      assertTrue(users.get(0).getName().contains("a"));
      users = userRepository.namedQueryByName("Charles");      assertTrue(users.size() == 1);      assertTrue(users.get(0).getId() == 104L);
      users = userRepository.nativeQueryByName("Mario");      assertTrue(users.size() == 1);      assertTrue(users.get(0).getId() == 105L);
   }

}


7. Rest services

And final thing: let's now run our application runner and open in browser: http://localhost:8080/users/

It should be something like :


{
  "_embedded" : {
    "users" : [ {
      "name" : "Joe",
      "salary" : 100,
      "_links" : {
        "self" : {
          "href" : "http://localhost:8080/users/101"
        },
        "user" : {
          "href" : "http://localhost:8080/users/101"
        },
        "group" : {
          "href" : "http://localhost:8080/users/101/group"
        }
      }
    }, {
      "name" : "Huan",
      "salary" : 200,
      "_links" : {
        "self" : {
          "href" : "http://localhost:8080/users/102"
        },
        "user" : {
          "href" : "http://localhost:8080/users/102"
        },
        "group" : {
          "href" : "http://localhost:8080/users/102/group"
        }
      }
    }, {
      "name" : "Michael",
      "salary" : 300,
      "_links" : {
        "self" : {
          "href" : "http://localhost:8080/users/103"
        },
        "user" : {
          "href" : "http://localhost:8080/users/103"
        },
        "group" : {
          "href" : "http://localhost:8080/users/103/group"
        }
      }
    }, {
      "name" : "Charles",
      "salary" : 100,
      "_links" : {
        "self" : {
          "href" : "http://localhost:8080/users/104"
        },
        "user" : {
          "href" : "http://localhost:8080/users/104"
        },
        "group" : {
          "href" : "http://localhost:8080/users/104/group"
        }
      }
    }, {
      "name" : "Mario",
      "salary" : 200,
      "_links" : {
        "self" : {
          "href" : "http://localhost:8080/users/105"
        },
        "user" : {
          "href" : "http://localhost:8080/users/105"
        },
        "group" : {
          "href" : "http://localhost:8080/users/105/group"
        }
      }
    }, {
      "name" : "Jan",
      "salary" : 300,
      "_links" : {
        "self" : {
          "href" : "http://localhost:8080/users/106"
        },
        "user" : {
          "href" : "http://localhost:8080/users/106"
        },
        "group" : {
          "href" : "http://localhost:8080/users/106/group"
        }
      }
    }, {
      "name" : "Victor",
      "salary" : 500,
      "_links" : {
        "self" : {
          "href" : "http://localhost:8080/users/107"
        },
        "user" : {
          "href" : "http://localhost:8080/users/107"
        },
        "group" : {
          "href" : "http://localhost:8080/users/107/group"
        }
      }
    } ]
  },
  "_links" : {
    "self" : {
      "href" : "http://localhost:8080/users{?page,size,sort}",
      "templated" : true
    },
    "profile" : {
      "href" : "http://localhost:8080/profile/users"
    },
    "search" : {
      "href" : "http://localhost:8080/users/search"
    }
  },
  "page" : {
    "size" : 20,
    "totalElements" : 7,
    "totalPages" : 1,
    "number" : 0
  }
}

- HATEOAS is in place  !

8. The end

As I mentioned at the beginning, with spring-boot stack we can create rest services with DB repositories by just adding few annotations! Full source code can be downloaded from here

Saturday, January 5, 2019

Spring MVC: unit and integration tests

1.Intro 

Spring MVC is the one of most popular framework for REST services in java. Let's see how can we test our Spring MVC rest serves.

2. Project structure

I created simple gradle spring boot project. It has beside main app starter  just one rest controller (UserController), two domain entities (Greeting, User) and test: unit and integration.





3. build.gradle

Beside spring-boot dependencies I just added Lombok to reduce some boilerplate code for domain objects. 

buildscript {
   ext {
      springBootVersion = '2.1.1.RELEASE'   }
   repositories {
      mavenCentral()
   }
   dependencies {
      classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
   }
}

apply plugin: 'java'apply plugin: 'eclipse'apply plugin: 'org.springframework.boot'apply plugin: 'io.spring.dependency-management'
group = 'com.demien'version = '0.0.1-SNAPSHOT'sourceCompatibility = 1.8
repositories {
   mavenCentral()
}


dependencies {
   implementation('org.springframework.boot:spring-boot-starter-web')
   compileOnly('org.projectlombok:lombok')
   testImplementation('org.springframework.boot:spring-boot-starter-test')
}

4. Main application starter

Nothing interesting is here. Just SpringApplication.run

package com.demien.sprmvc;
import org.springframework.boot.SpringApplication;import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplicationpublic class SprmvcApplication {

   public static void main(String[] args) {
      SpringApplication.run(SprmvcApplication.class, args);   }

}



5. Domain objects

Thanks to Lombok, my domain objects are really tiny!

 package com.demien.sprmvc.domain;

import java.util.Date;
import lombok.AllArgsConstructor;import lombok.Getter;import lombok.NoArgsConstructor;import lombok.Setter;
@Getter@Setter@NoArgsConstructor@AllArgsConstructorpublic class Greeting {
   private String message;   private Date dt;}


package com.demien.sprmvc.domain;
import lombok.AllArgsConstructor;import lombok.Getter;import lombok.NoArgsConstructor;import lombok.Setter;
@Getter@Setter@AllArgsConstructor@NoArgsConstructorpublic class User {
   private String name;}

6. Rest controller

Finally we've reached something interesting - the controller we are going to test.
I created several methods:  rest endpoints, from simple to complex:
 - "hello" - GET which is jest returning text result
 - "hello-with-object" - GET but it's returning java object. So object should be serialized and returned as JSON
- "hello-with-parameter" - GET which has a path paameter
- "helo-post" - POST which is receiving java object and returning java object as well. Of course these objects will be serialized to JSON. 



package com.demien.sprmvc.controller;
import java.net.URI;import java.util.Date;
import org.springframework.http.ResponseEntity;import org.springframework.stereotype.Controller;import org.springframework.web.bind.annotation.GetMapping;import org.springframework.web.bind.annotation.PathVariable;import org.springframework.web.bind.annotation.PostMapping;import org.springframework.web.bind.annotation.RequestBody;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.ResponseBody;import org.springframework.web.servlet.support.ServletUriComponentsBuilder;
import com.demien.sprmvc.domain.Greeting;import com.demien.sprmvc.domain.User;
@Controllerpublic class UserController {

   private static final String helloWorldTemplate = "Hello World, %s!";   private int id = 1;
   @RequestMapping(value = "/hello")
   public @ResponseBody String hello() {
      return "Hello world!";   }

   @GetMapping("/hello-with-object")
   public @ResponseBody Greeting helloWithObject() {
      return new Greeting("Hello World", new Date());   }

   @GetMapping("/hello-with-parameter/name/{name}")
   public @ResponseBody Greeting helloWithParameter(@PathVariable String name) {
      return new Greeting(String.format(helloWorldTemplate, name), new Date());   }

   @PostMapping("/hello-post")
   public ResponseEntity<?> postTest(@RequestBody User user) {
      Greeting result = new Greeting(String.format(helloWorldTemplate, user.getName()), new Date());      URI location = ServletUriComponentsBuilder.fromCurrentRequest().path("/{id}").buildAndExpand(id++).toUri();      return ResponseEntity.created(location).body(result);   }

}


7. Unit test

And now let's check if our controller works as expected. For unit tests we're using MockMVC. And also we need ObjectMapper for JSON serialization.
Please pay attention on this  annotation:

@WebMvcTest(UserController.class)

- our mockMvc will be created for UserController class. 


package com.demien.sprmvc.controller;
import static org.hamcrest.Matchers.containsString;import static org.hamcrest.Matchers.equalTo;import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import org.junit.Test;import org.junit.runner.RunWith;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;import org.springframework.http.MediaType;import org.springframework.test.context.junit4.SpringRunner;import org.springframework.test.web.servlet.MockMvc;import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import com.demien.sprmvc.domain.User;import com.fasterxml.jackson.databind.ObjectMapper;
@RunWith(SpringRunner.class)
@WebMvcTest(UserController.class)
public class UserControllerTest {

   @Autowired   private MockMvc mvc;
   @Autowired   private ObjectMapper mapper;
   @Test   public void helloTest() throws Exception {
      mvc.perform(MockMvcRequestBuilders.get("/hello").accept(MediaType.APPLICATION_JSON)).andExpect(status().isOk())
            .andExpect(content().string(equalTo("Hello world!")));   }

   @Test   public void helloWithObjectTest() throws Exception {
      mvc.perform(MockMvcRequestBuilders.get("/hello-with-object").accept(MediaType.APPLICATION_JSON))
            .andExpect(status().isOk()).andExpect(content().string(containsString("Hello World")));   }

   @Test   public void helloWithParameterTest() throws Exception {
      mvc.perform(MockMvcRequestBuilders.get("/hello-with-parameter/name/Buddy").accept(MediaType.APPLICATION_JSON))
            .andExpect(status().isOk()).andExpect(content().string(containsString("Hello World, Buddy")));   }

   @Test   public void postTest() throws Exception {
      User user = new User("Joe");      String userJson = mapper.writeValueAsString(user);      mvc.perform(
            MockMvcRequestBuilders.post("/hello-post").content(userJson).contentType(MediaType.APPLICATION_JSON))
            .andExpect(status().isCreated()).andExpect(content().string(containsString("Hello World, Joe")));   }

}


Results:



8. Integration test

For integration test we can not use any mocks - just real rest services. So we have to start our application
@SpringBootTest(classes = SprmvcApplication.class, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)

and  test needed rest enpoints using TestRestTemplate.

package com.demien.sprmvc;
import static org.hamcrest.MatcherAssert.assertThat;import static org.hamcrest.Matchers.containsString;import static org.hamcrest.Matchers.equalTo;
import org.junit.Test;import org.junit.runner.RunWith;import org.springframework.boot.test.context.SpringBootTest;import org.springframework.boot.test.web.client.TestRestTemplate;import org.springframework.boot.web.server.LocalServerPort;import org.springframework.http.ResponseEntity;import org.springframework.test.context.junit4.SpringRunner;
import com.demien.sprmvc.domain.Greeting;import com.demien.sprmvc.domain.User;
@RunWith(SpringRunner.class)
@SpringBootTest(classes = SprmvcApplication.class, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)

public class SprmvcApplicationIT {

   private static final String LOCAL_HOST = "http://localhost:";
   @LocalServerPort   private int port;   private TestRestTemplate template = new TestRestTemplate();
   @Test   public void helloTest() throws Exception {
      ResponseEntity<String> response = template.getForEntity(createURL("/hello"), String.class);      assertThat(response.getBody(), equalTo("Hello world!"));   }

   private String createURL(String uri) {
      return LOCAL_HOST + port + uri;   }

   @Test   public void helloWithObjectTest() throws Exception {
      ResponseEntity<String> response = template.getForEntity(createURL("/hello-with-object"), String.class);      assertThat(response.getBody(), containsString("Hello World"));   }

   @Test   public void helloWithParameterTest() throws Exception {
      ResponseEntity<String> response = template.getForEntity(createURL("/hello-with-parameter/name/Buddy"),            String.class);      assertThat(response.getBody(), containsString("Hello World, Buddy"));   }

   @Test   public void postTest() throws Exception {
      User userBean = new User("Joe");      ResponseEntity<Greeting> response = template.postForEntity(createURL("/hello-post"), userBean, Greeting.class);      Greeting result = response.getBody();      assertThat(result.getMessage(), containsString("Hello World, Joe"));   }

}


Results:




9. The end 

Unit and integration tests for out rest controller are in place, so we're good :)
Full source code can be downloaded from here.

Wednesday, September 27, 2017

Swagger with SpringBoot

From Wiki:
Swagger is an open source software framework backed by a large ecosystem of tools that helps developers design, build, document, and consume RESTful Web services. While most users identify Swagger by the Swagger UI tool, the Swagger toolset includes support for automated documentation, code generation, and test case generation.

Official page: https://swagger.io/


1. What is Swagger?

In this post I'll show 2 components of Swagger: 
   - set of annotations which help us to "describe" REST - related stuff: rest endpoints and DTO-objects.
   - Swagger UI, which can be used for calling this endpoints for testing purposes.

Example of "description" of DTO-object filed:   
@ApiModelProperty(notes = "Group Name")
private String name;

Example of "description" of REST-endpoint:
@GET@Path("/{id}")
@ApiOperation(value = "Get group by id resource.", response = Group.class)
@ApiResponses(value = {
        @ApiResponse(code = 200, message = "Group resource found"),
        @ApiResponse(code = 404, message = "Group resource not found")
})
public Response getGroup(@ApiParam @PathParam("id") Long id) {

Example of Swagger UI, using which we can call just listed above method: 




2. build.gradle 

I just generated the SpringBoot project from start.spring.io and added swagger dependency into it:
buildscript {
   ext {
      springBootVersion = '1.5.7.RELEASE'   }
   repositories {
      mavenCentral()
   }
   dependencies {
      classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
   }
}

apply plugin: 'java'apply plugin: 'eclipse'apply plugin: 'org.springframework.boot'
jar.archiveName = "SwaggerTestApp.jar"group = 'com.demien'version = '0.0.1-SNAPSHOT'sourceCompatibility = 1.8
repositories {
   mavenCentral()
}


dependencies {
   compile('org.springframework.boot:spring-boot-starter-jersey')
   compile('org.springframework.boot:spring-boot-starter-web')

    compile group: 'io.swagger', name: 'swagger-jersey2-jaxrs', version: '1.5.16'       testCompile('org.springframework.boot:spring-boot-starter-test')
}



3. Main start class


Nothing special here, just  adding several packages for scanning

package com.demien.swtest;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication(
    scanBasePackages = {
         "com.demien.swtest.config", 
         "com.demien.swtest.rest", 
         "com.demien.swtest.service"         }
)
public class SwtestApplication  {

   public static void main(String[] args) {
      SpringApplication.run(SwtestApplication.class, args);
   }
}



4. Jersey config


It's the most complicated part of application - we have to configure swagger here with metha-data of our application. 


package com.demien.swtest.config;

import com.demien.swtest.rest.GroupResource;
import io.swagger.jaxrs.config.BeanConfig;
import io.swagger.jaxrs.listing.ApiListingResource;
import io.swagger.jaxrs.listing.SwaggerSerializers;
import org.glassfish.jersey.server.ResourceConfig;
import org.glassfish.jersey.server.wadl.internal.WadlResource;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import javax.annotation.PostConstruct;

    @Component    public class JerseyConfig extends ResourceConfig {

        @Value("${spring.jersey.application-path:/}")
        private String apiPath;

        public JerseyConfig() {
            // Register endpoints, providers, ...            this.registerEndpoints();
        }

        @PostConstruct        public void init() {
            // Register components where DI is needed            this.configureSwagger();
            //this.registerEndpoints();        }

        private void registerEndpoints() {
            this.register(GroupResource.class);
            // Access through /<Jersey's servlet path>/application.wadl            this.register(WadlResource.class);
        }

        private void configureSwagger() {
            // Available at localhost:port/api/swagger.json            this.register(ApiListingResource.class);
            this.register(SwaggerSerializers.class);

            BeanConfig config = new BeanConfig();
            config.setConfigId("springboot-jersey-swagger-test-app");
            config.setTitle("Spring Boot, Jersey, Swagger Test Application");
            config.setVersion("v1");
            config.setContact("Dmitry Kovalsky");
            config.setSchemes(new String[] { "http", "https" });
            config.setBasePath(this.apiPath);
            config.setResourcePackage("com.demien.swtest.rest");
            config.setPrettyPrint(true);
            config.setScan(true);
        }
}


5. Dto and Model classes

UI is not sending ID - it will be generated on server side, what is why I need 2 classes: one for data which will be sent from UI and the second one  - for response. Fields in these classes are swagger-annotated.


package com.demien.swtest.dto;

import io.swagger.annotations.ApiModelProperty;

public class GroupDTO {

    @ApiModelProperty(notes = "Group Name")
    private String name;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

package com.demien.swtest.model;

import com.demien.swtest.dto.GroupDTO;
import io.swagger.annotations.ApiModelProperty;

public class Group extends GroupDTO{
    @ApiModelProperty(notes = "Generated Group ID")
    private Long id;

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public Group() {
    }

    public Group(GroupDTO dto) {
        setName(dto.getName());
    }

}



6. Rest controller(resource)

Here, all rest methods are swagger-annotated with description and errors which may be raised by it. 


package com.demien.swtest.rest;

import com.demien.swtest.dto.GroupDTO;
import com.demien.swtest.model.Group;
import com.demien.swtest.service.GroupService;
import io.swagger.annotations.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import javax.ws.rs.*;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriInfo;

@Component@Path("/groups")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
@Api(value = "Group resource", produces = "application/json")
public class GroupResource {

    @Autowired    private GroupService groupService;

    public Response OkResponse(Object entity) {
        return Response.status(Response.Status.OK).entity(entity).build();
    }

    public Response NotFoundResponse() {
        return Response.status(Response.Status.NOT_FOUND).build();
    }

    @POST    @ApiOperation(value = "Create group.", response = Group.class)
    @ApiResponses(value = {
            @ApiResponse(code = 201, message = "group resource ", responseHeaders = {
                    @ResponseHeader(name = "Location", description = "The URL to retrieve created resource", response = String.class)
            })
    })
    public Response createGroup(GroupDTO groupDTO, @Context UriInfo uriInfo) {
        Group result = groupService.add(new Group(groupDTO));
        return OkResponse(result);
    }

    @GET    @Path("/{id}")
    @ApiOperation(value = "Get group by id resource.", response = Group.class)
    @ApiResponses(value = {
            @ApiResponse(code = 200, message = "Group resource found"),
            @ApiResponse(code = 404, message = "Group resource not found")
    })
    public Response getGroup(@ApiParam @PathParam("id") Long id) {
        Group result = groupService.get(id);
        return result == null ? NotFoundResponse() : OkResponse(result);
    }

    @PUT    @ApiOperation(value = "Update group.", response = Group.class)
    @ApiResponses(value = {
            @ApiResponse(code = 200, message = "Group resource found")
    })
    public Response updateGroup(Group group, @Context UriInfo uriInfo) {
        groupService.update(group.getId(), group);
        Group result = groupService.get(group.getId());
        return OkResponse(result);
    }

    @DELETE    @Path("/{id}")
    @ApiOperation(value = "Delete group by id resource.")
    @ApiResponses(value = {
            @ApiResponse(code = 200, message = "Group resource found"),
            @ApiResponse(code = 404, message = "Group resource not found")
    })
    public Response deleteGroup(@ApiParam @PathParam("id") Long id) {
        Group result = groupService.get(id);
        if (result == null) return NotFoundResponse();
        groupService.delete(id);
        return OkResponse(id);
    }


}      

7. Imitation of service
I made imitation of generic service and concrete subclass. 


package com.demien.swtest.service;

import com.demien.swtest.model.Group;
import org.springframework.stereotype.Service;

import java.util.function.UnaryOperator;

@Servicepublic class GroupService extends AbstractService<Group> {

    public GroupService() {
        super((e, id) -> {
            e.setId(id);
            return e;
        });
    }
}



package com.demien.swtest.service;

import java.util.HashMap;
import java.util.Map;
import java.util.function.BiFunction;
import java.util.function.UnaryOperator;

public abstract class AbstractService<T> {

    private long id;
    private Map<Long,T> storage = new HashMap<>();
    private BiFunction<T, Long, T> idSetter;

    public AbstractService(BiFunction<T, Long, T> idSetter) {
        this.idSetter = idSetter;
    }


    public T add(T entity) {
        id++;
        T result =  idSetter.apply(entity, id);
        storage.put(id, result);
        return result;

    }

    public T get(Long id) {
        return storage.get(id);
    }

    public void update(Long id, T entity) {
        storage.put(id, entity);
    }

    public void delete(Long id) {
        storage.put(id, null);
    }
}



8. swagger.json

Now we can run our application and open this address: http://localhost:8080/api/swagger.json
The result should be - json generated by swagger which "explains" our rest endpoints with 
detailed description: 

{
   "swagger":"2.0",
   "info":{
      "version":"v1",
      "title":"Spring Boot, Jersey, Swagger Test Application",
      "contact":{
         "name":"Dmitry Kovalsky"
      }
   },
   "basePath":"/api",
   "tags":[
      {
         "name":"Group resource"
      }
   ],
   "schemes":[
      "http",
      "https"
   ],
   "paths":{
      "/groups/{id}":{
         "get":{
            "tags":[
               "Group resource"
            ],
            "summary":"Get group by id resource.",
            "description":"",
            "operationId":"getGroup",
            "consumes":[
               "application/json"
            ],
            "produces":[
               "application/json"
            ],
            "parameters":[
               {
                  "name":"id",
                  "in":"path",
                  "required":true,
                  "type":"integer",
                  "format":"int64"
               }
            ],
            "responses":{
               "200":{
                  "description":"Group resource found"
               },
               "404":{
                  "description":"Group resource not found"
               }
            }
         },
         "delete":{
            "tags":[
               "Group resource"
            ],
            "summary":"Delete group by id resource.",
            "description":"",
            "operationId":"deleteGroup",
            "consumes":[
               "application/json"
            ],
            "produces":[
               "application/json"
            ],
            "parameters":[
               {
                  "name":"id",
                  "in":"path",
                  "required":true,
                  "type":"integer",
                  "format":"int64"
               }
            ],
            "responses":{
               "200":{
                  "description":"Group resource found"
               },
               "404":{
                  "description":"Group resource not found"
               }
            }
         }
      },
      "/groups":{
         "post":{
            "tags":[
               "Group resource"
            ],
            "summary":"Create group.",
            "description":"",
            "operationId":"createGroup",
            "consumes":[
               "application/json"
            ],
            "produces":[
               "application/json"
            ],
            "parameters":[
               {
                  "in":"body",
                  "name":"body",
                  "required":false,
                  "schema":{
                     "$ref":"#/definitions/GroupDTO"
                  }
               }
            ],
            "responses":{
               "200":{
                  "description":"successful operation",
                  "schema":{
                     "$ref":"#/definitions/Group"
                  }
               },
               "201":{
                  "description":"group resource ",
                  "headers":{
                     "Location":{
                        "type":"string",
                        "description":"The URL to retrieve created resource"
                     }
                  }
               }
            }
         },
         "put":{
            "tags":[
               "Group resource"
            ],
            "summary":"Update group.",
            "description":"",
            "operationId":"updateGroup",
            "consumes":[
               "application/json"
            ],
            "produces":[
               "application/json"
            ],
            "parameters":[
               {
                  "in":"body",
                  "name":"body",
                  "required":false,
                  "schema":{
                     "$ref":"#/definitions/Group"
                  }
               }
            ],
            "responses":{
               "200":{
                  "description":"Group resource found"
               }
            }
         }
      }
   },
   "definitions":{
      "Group":{
         "type":"object",
         "properties":{
            "name":{
               "type":"string"
            },
            "id":{
               "type":"integer",
               "format":"int64"
            }
         }
      },
      "GroupDTO":{
         "type":"object",
         "properties":{
            "name":{
               "type":"string"
            }
         }
      }
   }
}


9. Swagger UI 

JSON with endpoint description - is great! But swagger can even more: based on this JSON, it can provide the UI to call these endpoints. We just have to download it from https://swagger.io/swagger-ui/ and put into src/main/resource/static. In will be available by address: http://localhost:8080/index.html.

Example of trying POST method on GROUP resource:


And after pressing "Try it out!" we will have: 



10. The end


Full source code can be downloaded from here