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