Investigating Springfox and Swagger UI, but I am facing an issue. I am using the Spring Boot REST example project as the foundation for my PoC. I'm running JDK 8 and the project leverages Gradle.
First, here are the file contents for the project:
build.gradle
buildscript {
repositories {
mavenCentral()
}
dependencies {
classpath("org.springframework.boot:spring-boot-gradle-plugin:1.2.7.RELEASE")
}
}
apply plugin: 'java'
apply plugin: 'eclipse'
apply plugin: 'idea'
apply plugin: 'spring-boot'
jar {
baseName = 'gs-rest-service'
version = '0.1.0'
}
repositories {
mavenCentral()
jcenter()
}
sourceCompatibility = 1.8
targetCompatibility = 1.8
dependencies {
compile("org.springframework.boot:spring-boot-starter-web")
compile("io.springfox:springfox-swagger2:2.2.2")
compile("io.springfox:springfox-swagger-ui:2.2.2")
testCompile("junit:junit")
}
task wrapper(type: Wrapper) {
gradleVersion = '2.3'
}
GreetingController.java
package hello;
import java.util.concurrent.atomic.AtomicLong;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
#RestController
public class GreetingController {
private static final String template = "Hello, %s!";
private final AtomicLong counter = new AtomicLong();
#RequestMapping("/greeting")
public Greeting greeting(#RequestParam(value="name", defaultValue="World") String name) {
return new Greeting(counter.incrementAndGet(),
String.format(template, name));
}
}
Greeting.java
package hello;
public class Greeting {
private final long id;
private final String content;
public Greeting(long id, String content) {
this.id = id;
this.content = content;
}
public long getId() {
return id;
}
public String getContent() {
return content;
}
}
Application.java
package hello;
import static com.google.common.collect.Lists.newArrayList;
import static springfox.documentation.schema.AlternateTypeRules.newRule;
import java.time.LocalDate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.context.request.async.DeferredResult;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.builders.ResponseMessageBuilder;
import springfox.documentation.schema.ModelRef;
import springfox.documentation.schema.WildcardType;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2;
import com.fasterxml.classmate.TypeResolver;
#SpringBootApplication
#EnableSwagger2
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
#Autowired
private TypeResolver typeResolver;
#Bean
public Docket greetingApi() {
return new Docket(DocumentationType.SPRING_WEB)
.select()
.apis(RequestHandlerSelectors.any())
.paths(PathSelectors.any())
.build()
.pathMapping("/")
.directModelSubstitute(LocalDate.class, String.class)
.genericModelSubstitutes(ResponseEntity.class)
.alternateTypeRules(newRule(typeResolver.resolve(DeferredResult.class,
typeResolver.resolve(ResponseEntity.class, WildcardType.class)),
typeResolver.resolve(WildcardType.class)))
.useDefaultResponseMessages(false)
.globalResponseMessage(RequestMethod.GET,
newArrayList(new ResponseMessageBuilder()
.code(500)
.message("500 message")
.responseModel(new ModelRef("Error"))
.build()))
.enableUrlTemplating(true);
}
}
Here is the issue I am facing. When I build and run the application, I can successfully navigate to the Swagger UI page (http://localhost:8080/swagger-ui.html). When I expand greeting-controller, I see the different methods and expand "get /greeting{?name}". The Get section has the following content:
Response Class (Status 200)
Model
{
"content": "string",
"id": 0
}
Response Content Type: */*
Parameters
parameter = name, value = World, parameter type = query, data type = string
When I click the "Try It Out" button, I see the following:
curl = curl -X GET --header "Accept: */*" "http://localhost:8080/greeting{?name}?name=World"
request url = http://localhost:8080/greeting{?name}?name=World
repsonse body = {
"timestamp": 1446418006199,
"status": 404,
"error": "Not Found",
"message": "No message available",
"path": "/greeting%7B"
}
response code = 404
response headers = {
"server": "Apache-Coyote/1.1",
"content-type": "application/json;charset=UTF-8",
"transfer-encoding": "chunked",
"date": "Sun, 01 Nov 2015 22:46:46 GMT"
}
At first glance, it looks like for some reason that Springfox/Swagger is not correctly replacing the placeholder for {?name}. My question is, how do I configure it to do so, if that is in fact the issue, so that I can successfully test out the service from the Swagger UI page?
In your Application class changing the enableUrlTemplating to false will fix your problem.
#Bean
public Docket greetingApi() {
return new Docket(DocumentationType.SPRING_WEB)
//...
.enableUrlTemplating(false);
}
Just a little bit of background on that flag. That flag is to support RFC 6570 without which operations that differ only by query string parameters will not show up correctly per spec. In the next iteration of the swagger spec there are plans to address that issue. That is the reason for enableUrlTemplating to be marked as an incubating feature.
Related
Swagger Configuration not working
I use swagger with springboot , i am getting error404 when i hit this
url http://localhost:8080/swagger-ui.html. I am new to swagger I have
no idea how to solve it. I create separate configuration file
Failed to load resource: the server responded with a status of 404 ()
Here all the documentation details
UserCongig.kt
package com.main.swaggerdemo.config
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import springfox.documentation.builders.PathSelectors
import springfox.documentation.builders.RequestHandlerSelectors
import springfox.documentation.spi.DocumentationType
import springfox.documentation.spring.web.plugins.Docket
import springfox.documentation.swagger2.annotations.EnableSwagger2
#Configuration
#EnableSwagger2
class UserConfig {
#Bean
fun postApi(): Docket {
return Docket(DocumentationType.SWAGGER_2)
.select()
.paths(PathSelectors.ant("/api/*"))
.apis(RequestHandlerSelectors.basePackage("com.main.swaggerdemo"))
.build()
}
}
UserController.kt
package com.main.swaggerdemo.controller
import com.main.swaggerdemo.entity.User
import com.main.swaggerdemo.model.req.ReqUser
import com.main.swaggerdemo.model.response.RespUser
import com.main.swaggerdemo.repo.UserRepo
import io.swagger.annotations.ApiOperation
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.http.HttpStatus
import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.*
#RestController
class UserController {
#Autowired
private lateinit var userRepo: UserRepo
#GetMapping("/signup")
#ApiOperation(value = "create new user")
fun signupUser(#ModelAttribute request: ReqUser): ResponseEntity<*> {
val newUser = User(name = request.name, country = request.country,
email = request.email, password = request.password)
userRepo.save(newUser)
val resUser = RespUser(newUser.id, newUser.name, newUser.country, newUser.email)
return ResponseEntity(resUser, HttpStatus.OK)
}
#GetMapping("/findByEmail/{email}")
#ApiOperation(value = "find the user")
fun getUserByEmail(#PathVariable("email") email: String): ResponseEntity<*> {
val curentUser = userRepo.findByEmail(email)
if (curentUser != null) {
val userData = userRepo.findByEmail(email)
return ResponseEntity(userData, HttpStatus.OK)
}
return ResponseEntity("No data found", HttpStatus.NOT_FOUND)
}
}
build-gradle.kts
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
plugins {
id("org.springframework.boot") version "2.4.5"
id("io.spring.dependency-management") version "1.0.11.RELEASE"
kotlin("jvm") version "1.4.32"
kotlin("plugin.spring") version "1.4.32"
kotlin("plugin.jpa") version "1.4.32"
}
group = "com.main"
version = "0.0.1-SNAPSHOT"
java.sourceCompatibility = JavaVersion.VERSION_11
repositories {
mavenCentral()
}
dependencies {
implementation("org.springframework.boot:spring-boot-starter-data-jpa")
implementation("org.springframework.boot:spring-boot-starter-web")
implementation("io.springfox:springfox-swagger2:3.0.0")
implementation("io.springfox:springfox-swagger-ui:3.0.0")
implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
implementation("org.jetbrains.kotlin:kotlin-reflect")
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
runtimeOnly("mysql:mysql-connector-java")
testImplementation("org.springframework.boot:spring-boot-starter-test")
}
tasks.withType<KotlinCompile> {
kotlinOptions {
freeCompilerArgs = listOf("-Xjsr305=strict")
jvmTarget = "11"
}
}
tasks.withType<Test> {
useJUnitPlatform()
}
I have added following changes for my service with swagger. You can try the same.
#Configuration
#EnableSwagger2
class UserConfig {
#Bean
public Docket apiDocumentationV1() {
return new Docket(DocumentationType.SWAGGER_2)
.select()
.paths(PathSelectors.ant("/v1/**"))
.build()
.apiInfo(createMetaData())
.useDefaultResponseMessages(false);
}
private ApiInfo createMetaData() {
return new ApiInfoBuilder()
.title("Test Service APIs")
.description("API to maintain test service.")
.version("1.0.0")
.build();
}
}
I coded a very simple and common CRUD in Kotlin. I want to do basic tests as testing post, delete, get and put.
Probably I understood something wrong: I used Beforeeach aimed to insert a register so I could check during get test. I don't get exception but it seems during get test it always returning ok when it should be NOT_FOUND for any other id different than 1 in bellow test.
Any clue or guidance in right direction will be wellcome even if see other bad practice bellow based on my purpose (simple CRUD test).
test
package com.mycomp.jokenpo
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.mycomp.jokenpo.controller.UserController
import com.mycomp.jokenpo.model.User
import com.mycomp.jokenpo.respository.UserRepository
import com.mycomp.jokenpo.service.UserService
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.ExtendWith
import org.mockito.InjectMocks
import org.mockito.Mock
import org.mockito.Mockito.`when`
import org.mockito.MockitoAnnotations
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.boot.test.web.client.TestRestTemplate
import org.springframework.http.HttpStatus
import org.springframework.http.MediaType
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter
import org.springframework.test.context.junit.jupiter.SpringExtension
import org.springframework.test.web.servlet.MockMvc
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders
import org.springframework.test.web.servlet.result.MockMvcResultHandlers
import org.springframework.test.web.servlet.result.MockMvcResultMatchers
import org.springframework.test.web.servlet.setup.MockMvcBuilders
#SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
#ExtendWith(SpringExtension::class)
#AutoConfigureMockMvc
class JokenpoApplicationTests {
#Autowired
lateinit var testRestTemplate: TestRestTemplate
#Autowired
private lateinit var mvc: MockMvc
#InjectMocks
lateinit var controller: UserController
#Mock
lateinit var respository: UserRepository
#Mock
lateinit var service: UserService
//private fun <T> any(type: Class<T>): T = Mockito.any<T>(type)
#BeforeEach
fun setup() {
MockitoAnnotations.initMocks(this)
mvc = MockMvcBuilders.standaloneSetup(controller).setMessageConverters(MappingJackson2HttpMessageConverter()).build()
`when`(respository.save(User(1, "Test")))
.thenReturn(User(1, "Test"))
}
#Test
fun createUser() {
//val created = MockMvcResultMatchers.status().isCreated
var user = User(2, "Test")
var jsonData = jacksonObjectMapper().writeValueAsString(user)
mvc.perform(MockMvcRequestBuilders.post("/users/")
.contentType(MediaType.APPLICATION_JSON)
.content(jsonData))
.andExpect(MockMvcResultMatchers.status().isOk)
//.andExpect(created)
.andDo(MockMvcResultHandlers.print())
.andReturn()
}
#Test
fun findUser() {
val ok = MockMvcResultMatchers.status().isOk
val builder = MockMvcRequestBuilders.get("/users?id=99") //no matther which id I type here it returns ok. I would expect only return for 1 based on my #BeforeEach
this.mvc.perform(builder)
.andExpect(ok)
}
}
controller
package com.mycomp.jokenpo.controller
import com.mycomp.jokenpo.model.User
import com.mycomp.jokenpo.respository.UserRepository
import com.mycomp.jokenpo.service.UserService
import org.springframework.http.HttpStatus
import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.*
import java.util.concurrent.atomic.AtomicLong
import javax.validation.Valid
#RestController
#RequestMapping("users")
class UserController (private val userService: UserService, private val userRepository: UserRepository){
val counter = AtomicLong()
// #GetMapping("/user")
// fun getUser(#RequestParam(value = "name", defaultValue = "World") name: String) =
// User(counter.incrementAndGet(), "Hello, $name")
#GetMapping()
fun getAllUsers(): List<User> =
userService.all()
#PostMapping
fun add(#Valid #RequestBody user: User): ResponseEntity<User> {
//user.id?.let { userService.save(it) }
val savedUser = userService.save(user)
return ResponseEntity.ok(savedUser)
}
#GetMapping("/{id}")
fun getUserById(#PathVariable(value = "id") userId: Long): ResponseEntity<User> {
return userRepository.findById(userId).map { user ->
ResponseEntity.ok(user)
}.orElse(ResponseEntity.notFound().build())
}
#DeleteMapping("/{id}")
fun deleteUserById(#PathVariable(value = "id") userId: Long): ResponseEntity<Void> {
return userRepository.findById(userId).map { user ->
userRepository.deleteById(user.id)
ResponseEntity<Void>(HttpStatus.OK)
}.orElse(ResponseEntity.notFound().build())
}
// #DeleteMapping("{id}")
// fun deleteUserById(#PathVariable id: Long): ResponseEntity<Unit> {
// if (noteService.existsById(id)) {
// noteService.deleteById(id)
// return ResponseEntity.ok().build()
// }
// return ResponseEntity.notFound().build()
// }
/////
// #PutMapping("{id}")
// fun alter(#PathVariable id: Long, #RequestBody user: User): ResponseEntity<User> {
// return userRepository.findById(userId).map { user ->
// userRepository. deleteById(user.id)
// ResponseEntity<Void>(HttpStatus.OK)
// }.orElse(ResponseEntity.notFound().build())
// }
}
Repository
package com.mycomp.jokenpo.respository
import com.mycomp.jokenpo.model.User
import org.springframework.data.repository.CrudRepository
interface UserRepository : CrudRepository<User, Long>
Model
package com.mycomp.jokenpo.model
import javax.persistence.*
#Entity
data class User(
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
val id: Long,
#Column(nullable = false)
val name: String
)
gradle dependencies
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
plugins {
id("org.springframework.boot") version "2.2.6.RELEASE"
id("io.spring.dependency-management") version "1.0.9.RELEASE"
kotlin("jvm") version "1.3.71"
kotlin("plugin.spring") version "1.3.71"
kotlin("plugin.jpa") version "1.3.71"
}
group = "com.mycomp"
version = "0.0.1-SNAPSHOT"
java.sourceCompatibility = JavaVersion.VERSION_1_8
val developmentOnly by configurations.creating
configurations {
runtimeClasspath {
extendsFrom(developmentOnly)
}
}
repositories {
mavenCentral()
}
dependencies {
implementation("org.springframework.boot:spring-boot-starter-data-jpa")
implementation("org.springframework.boot:spring-boot-starter-web")
implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
implementation("org.jetbrains.kotlin:kotlin-reflect")
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
developmentOnly("org.springframework.boot:spring-boot-devtools")
runtimeOnly("com.h2database:h2")
//runtimeOnly("org.hsqldb:hsqldb")
testImplementation("org.springframework.boot:spring-boot-starter-test") {
exclude(group = "org.junit.vintage", module = "junit-vintage-engine")
}
testImplementation ("com.nhaarman.mockitokotlin2:mockito-kotlin:2.2.0")
}
tasks.withType<Test> {
useJUnitPlatform()
}
tasks.withType<KotlinCompile> {
kotlinOptions {
freeCompilerArgs = listOf("-Xjsr305=strict")
jvmTarget = "1.8"
}
}
application.yml
spring:
datasource:
url: jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE
username: sa
password:
driver-class-name: org.h2.Driver
platform: h2
h2:
console:
enabled: true
path: /h2-console #jdbc:h2:mem:testdb
In case it is usefull the whole project can be dowloaded from https://github.com/jimisdrpc/games but I am confident that all files above are enough to ilustrate my issue.
To solve your problem I suggest using #MockBean, an annotation that can be used to add mocks to a Spring ApplicationContext.
I would re-write your test as follows (notice that I'm taking advantage of mockito-kotlin already being a test dependency of your project):
package com.mycomp.jokenpo
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.mycomp.jokenpo.model.User
import com.mycomp.jokenpo.respository.UserRepository
import com.nhaarman.mockitokotlin2.whenever
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.assertThrows
import org.junit.jupiter.api.extension.ExtendWith
import org.mockito.junit.jupiter.MockitoExtension
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.boot.test.mock.mockito.MockBean
import org.springframework.http.MediaType
import org.springframework.test.context.junit.jupiter.SpringExtension
import org.springframework.test.web.servlet.MockMvc
import org.springframework.test.web.servlet.get
import org.springframework.test.web.servlet.post
import org.springframework.web.util.NestedServletException
#AutoConfigureMockMvc. // auto-magically configures and enables an instance of MockMvc
#SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
// Why configure Mockito manually when a JUnit 5 test extension already exists for that very purpose?
#ExtendWith(SpringExtension::class, MockitoExtension::class)
class JokenpoApplicationTests {
#Autowired
private lateinit var mockMvc: MockMvc
#MockBean
lateinit var respository: UserRepository
#BeforeEach
fun setup() {
// use mockito-kotlin for a more idiomatic way of setting up your test expectations
whenever(respository.save(User(1, "Test"))).thenAnswer {
it.arguments.first()
}
}
#Test
fun `Test createUser in the happy path scenario`() {
val user = User(1, "Test")
mockMvc.post("/users/") {
contentType = MediaType.APPLICATION_JSON
content = jacksonObjectMapper().writeValueAsString(user)
accept = MediaType.APPLICATION_JSON
}.andExpect {
status { isOk }
content { contentType(MediaType.APPLICATION_JSON) }
content { json("""{"id":1,"name":"Test"}""") }
}
verify(respository, times(1)).save(user)
}
#Test
fun `Test negative scenario of createUser`() {
val user = User(2, "Test")
assertThrows<NestedServletException> {
mockMvc.post("/users/") {
contentType = MediaType.APPLICATION_JSON
content = jacksonObjectMapper().writeValueAsString(user)
accept = MediaType.APPLICATION_JSON
}
}
verify(respository, times(1)).save(user)
}
#Test
fun findUser() {
mockMvc.get("/users?id=99")
.andExpect {
status { isOk }
}
verify(respository, times(1)).findAll()
}
}
Having said that, here's some food for thought:
Any test needs to include verification to assert that the systems behaves as is expected under various types of scenarios including negative scenarios such as how do we check if the service failed to create a new User record in the DB.
I noticed you already have a Test DB setup in your ApplicationContext (H2) so why not use it to create test records instead of just mocking the repository layer? Then you can verify the DB contains any newly created records.
As a general rule, I avoid using Mockito with Kotlin tests (search StackOverflow for a couple of reasons why), or even mockito-kotlin. Best practice nowadays is to use the excellent MockK library in combination with either AssertJ or assertk for verifying your expectations.
To run get unit tests running in following setup:
- Kotlin
- Spring Boot
- JUnit 5
- Mockito
- Gradle
you need this configuration, to get started:
build.gradle.kts
dependencies {
// ...
testRuntimeOnly(group = "org.junit.jupiter", name = "junit-jupiter-engine", version = "5.6.3")
testImplementation(group = "org.mockito", name = "mockito-all", version = "1.10.19")
testImplementation("org.springframework.boot:spring-boot-starter-test") {
exclude(group = "org.junit.vin tage", module = "junit-vintage-engine")
}
// ...
}
tasks.withType<Test> {
useJUnitPlatform()
}
test file
#org.springframework.boot.test.context.SpringBootTest
class YourTest {
#org.mockito.Mock
lateinit var testingRepo: TestingRepo
#org.mockito.InjectMocks
lateinit var testingService: TestingService
#org.springframework.test.context.event.annotation.BeforeTestMethod
fun initMocks() {
org.mockito.MockitoAnnotations.initMocks(this)
}
#org.junit.jupiter.api.Test
fun yourTest() {org.junit.jupiter.api.Assertions.assertTrue(true)}
}
I am trying to use spring-boot and gradle to create a simple CRUD application as a PoC. At this point, I am getting this error when I am attempting to load the front website-
2019-02-11 14:54:48.915 WARN 5704 --- [nio-8080-exec-3] o.s.web.servlet.PageNotFound : No mapping for GET /WEB-INF/jsp/welcome.jsp
I am using spring boot and gradle by specifying in the controller class, and the application properties and build gradle script but still I get this debug statement when I go to my welcome.jsp and the now corresponding 404 error.
I've already looked through a lot of tutorials, but a lot of them are in maven. At this point, I know I need to specify the path in the controller and specify the resources in the properties and build scripts. I have even tried creating a specific mvcconfiguration but I don't think I need one.
build.gradle
buildscript {
ext {
springBootVersion = '2.1.2.RELEASE'
}
repositories {
mavenCentral()
}
dependencies {
classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
}}
apply plugin: 'eclipse'
apply plugin: 'java'
apply plugin: 'org.springframework.boot'
apply plugin: 'io.spring.dependency-management'
group = 'com.bill'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '1.8'
repositories {
mavenCentral()
maven { url 'https://repo.spring.io/milestone' }
}
ext {
set('springCloudServicesVersion', '2.1.0.RELEASE')
set('springCloudVersion', 'Greenwich.RELEASE')
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-validation'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-actuator'
/*implementation 'io.pivotal.spring.cloud:spring-cloud-services-starter-config-client'
/*implementation 'io.pivotal.spring.cloud:spring-cloud-services-starter-service-registry'
implementation 'org.springframework.cloud:spring-cloud-starter'
/*implementation 'org.springframework.cloud:spring-cloud-starter-netflix-eureka-client'
implementation 'org.springframework.cloud:spring-cloud-starter-netflix-eureka-server'
implementation 'org.springframework.cloud:spring-cloud-starter-netflix-ribbon'
*/
runtimeOnly 'mysql:mysql-connector-java'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
}
dependencyManagement {
imports {
mavenBom "io.pivotal.spring.cloud:spring-cloud-services-dependencies:${springCloudServicesVersion}"
mavenBom "org.springframework.cloud:spring-cloud-dependencies:${springCloudVersion}"
}
}
application.properties
spring.mvc.view.prefix:/WEB-INF/jsp/
spring.mvc.view.suffix:.jsp
spring.jpa.hibernate.ddl-auto=update
spring.datasource.url=jdbc:mysql://localhost:3306/mh1
spring.datasource.username=root
spring.datasource.password=root
MainController.java
package com.javainuse;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
import com.javainuse.BankAccountApplication;
import com.javainuse.User;
import com.javainuse.UserDAO;
import com.javainuse.UserForm;
#Controller
public class MainController {
#Autowired
UserDAO userDAO;
#RequestMapping("/welcome")
public ModelAndView viewHome() {
ModelAndView modelAndView = new ModelAndView();
modelAndView.setViewName("welcome");
return modelAndView;
}
#RequestMapping("/users")
public String viewUsers(Model model) {
List<User> list = UserDAO.getUsers();
model.addAttribute("users", list);
return "usersPage";
}
#RequestMapping("/registerSuccessful")
public String viewRegisterSuccessful() {
return "registerSuccessfulPage";
}
#RequestMapping(value = "/register", method = RequestMethod.GET)
public String viewRegister(Model model) {
UserForm form = new UserForm();
model.addAttribute("userForm", form);
return "registerPage";
}
public String saveRegister( Model model, //
#ModelAttribute("userForm") #Validated UserForm userForm, //
BindingResult result, //
final RedirectAttributes redirectAttributes) {
// Validate result
if (result.hasErrors()) {
return "registerPage";
}
User newUser= null;
try {
newUser = userDAO.createUser(userForm);
}
// Other error!!
catch (Exception e) {
model.addAttribute("errorMessage", "Error: " + e.getMessage());
return "registerPage";
}
redirectAttributes.addFlashAttribute("newUser", newUser);
return "redirect:/registerSuccessful";
}
}
MvcConfiguration.java
package com.javainuse;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
import org.springframework.web.servlet.view.InternalResourceViewResolver;
#Configuration
#ComponentScan(basePackages="...package name...")
#EnableWebMvc
public class MvcConfiguration extends WebMvcConfigurerAdapter{
#Bean
public InternalResourceViewResolver viewResolver() {
InternalResourceViewResolver viewResolver = new InternalResourceViewResolver();
viewResolver.setPrefix("WEB-INF/jsp/");
viewResolver.setSuffix(".jsp");
return viewResolver;
}
}
2019-02-11 14:54:48.915 WARN 5704 --- [nio-8080-exec-3] o.s.web.servlet.PageNotFound : No mapping for GET /WEB-INF/jsp/welcome.jsp
I am attempting to use Spring's auto-configuration to spin up an embedded LDAP server and access it using spring-data-ldap. However, the autowired fields, repository (an instance of LdapRepository) and the ldapTemplate (an instance of the LdapTemplate) are null.
For example,
spring.ldap.model.UserTest > testSaveUser FAILED
java.lang.NullPointerException at UserTest.java:32
What am I missing?
build.gradle
plugins {
id 'org.springframework.boot' version '2.0.6.RELEASE' apply false
}
apply plugin: 'io.spring.dependency-management'
apply plugin: 'java'
repositories {
jcenter()
}
dependencyManagement {
imports {
mavenBom org.springframework.boot.gradle.plugin.SpringBootPlugin.BOM_COORDINATES
}
}
dependencies {
compile 'org.springframework.data:spring-data-ldap'
compile 'com.unboundid:unboundid-ldapsdk'
testCompile 'org.springframework.boot:spring-boot-starter-data-ldap'
testCompile 'org.springframework.boot:spring-boot-starter-test'
}
sourceCompatibility = '1.8'
[compileJava, compileTestJava]*.options*.encoding = 'UTF-8'
Under src/main/java/spring/ldap/model:
Config.java
package spring.ldap.model;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.ldap.repository.config.EnableLdapRepositories;
#Configuration
#EnableLdapRepositories
public class Config { }
User.java
package spring.ldap.model;
import javax.naming.Name;
import org.springframework.ldap.odm.annotations.Attribute;
import org.springframework.ldap.odm.annotations.DnAttribute;
import org.springframework.ldap.odm.annotations.Entry;
import org.springframework.ldap.odm.annotations.Id;
#Entry(objectClasses = { "person", "top" }, base="dc=spring,dc=ldap,dc=model" )
public class User {
#Id private Name dn;
#Attribute(name = "cn") #DnAttribute(value = "cn", index = 0) private String userName;
#Attribute(name = "sn") private String fullName;
public Name getDn() { return dn; }
public void setDn(Name dn) { this.dn = dn; }
public String getUsername() { return userName; }
public void setUsername(String userName) { this.userName = userName; }
public String getFullName() { return fullName; }
public void setFullName(String fullName) { this.fullName = fullName; }
}
UserRepository.java
package spring.ldap.model;
import org.springframework.data.ldap.repository.LdapRepository;
import org.springframework.stereotype.Repository;
#Repository
public interface UserRepository extends LdapRepository<User> { }
Under src/test/java/spring/ldap/model:
UserTest.java
package spring.ldap.model;
import static org.junit.Assert.assertNotNull;
import javax.naming.Name;
import org.junit.Before;
import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.data.ldap.DataLdapTest;
import org.springframework.ldap.core.DirContextAdapter;
import org.springframework.ldap.core.LdapTemplate;
import org.springframework.ldap.support.LdapNameBuilder;
import org.springframework.test.context.ContextConfiguration;
#DataLdapTest()
#ContextConfiguration(classes = Config.class)
public class UserTest {
#Autowired UserRepository repository;
#Autowired LdapTemplate ldapTemplate;
#Before
public void SetUp() {
LdapNameBuilder builder = LdapNameBuilder.newInstance();
builder.add("dc", "model");
builder.add("dc", "ldap");
builder.add("dc", "spring");
Name dn = builder.build();
DirContextAdapter context = new DirContextAdapter(dn);
context.setAttributeValues("objectclass", new String[] { "top", "domain", "extensibleObject" });
context.setAttributeValue("dc", "spring");
ldapTemplate.bind(context);
}
#Test public void testSaveUser() {
User user = new User();
user.setUsername("ncornish");
user.setFullName("Nicholas Cornish");
repository.save(user);
assertNotNull("Id was not generated!", user.getDn());
}
}
Under src/test/resources:
application.properties
spring.ldap.embedded.base-dn=dc=spring,dc=ldap,dc=model
spring.ldap.embedded.credential.username=uid=admin
spring.ldap.embedded.credential.password=secret
Must put these annotations on test class context:
#RunWith(SpringRunner.class)
#SpringBootTest(classes = { UserRepository.class, LdapTemplate.class })
#ContextConfiguration(classes = Config.class)
#EnableConfigurationProperties
Kind regards.
Thanks JB Nezit, your comment
Your test doesn't run into a Spring context, since it's not annotated
with #RunWith(SpringRunner.class). So nothing will ever autowire
anything inside it.
resolves the issue. ie. add the following to the test class
#RunWith(SpringRunner.class)
Problem: Rule(InterestRate.drl) getting fired in standalone java code and giving me accurate result
package com.test.drool.config;
import org.kie.api.KieServices;
import org.kie.api.runtime.KieContainer;
import org.kie.api.runtime.KieSession;
import com.test.drool.facts.InterestRate;
public class RunSampleInterestRate {
public static void main(String[] args) {
// TODO Auto-generated method stub
KieContainer container= KieServices.Factory.get().getKieClasspathContainer();
InterestRate interestRate=new InterestRate();
interestRate.setLender("RBL");
System.out.println("printing session object before inserting"+interestRate.toString());
KieSession kieSession=container.newKieSession("ksession-rules");
kieSession.insert(interestRate);
kieSession.fireAllRules();
System.out.println(interestRate.getRate());
}
}
gives me expected 12.5 as interest rate.
Problem:I have to integrate this in rest service and I have been trying to test same logic under rest environment and it is not giving me expected results.After firing rules,service
always returns default value 0.0.My Environment is Spring-boot and drool is 6.5.0 final.
POJO:
package com.test.drool.facts;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
#JsonIgnoreProperties(ignoreUnknown=true)
public class InterestRate {
#Override
public String toString() {
return "InterestRate [lender=" + lender + ", principal=" + principal + ", store=" + store
+ ", dealer=" + dealer + ", rate=" + rate + "]";
}
private String lender;
private String principal;
private String store;
private String dealer;
private double rate;
public double getRate() {
return rate;
}
public void setRate(double rate) {
this.rate = rate;
}
public String getLender() {
return lender;
}
public void setLender(String lender) {
this.lender = lender;
}
public String getPrincipal() {
return principal;
}
public void setPrincipal(String principal) {
this.principal = principal;
}
public String getStore() {
return store;
}
public void setStore(String store) {
this.store = store;
}
public String getDealer() {
return dealer;
}
public void setDealer(String dealer) {
this.dealer = dealer;
}
}
Bean Config
package com.test.drool.config;
import org.kie.api.KieServices;
import org.kie.api.runtime.KieContainer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
#Configuration
public class DroolDependencyConfig {
#Bean(name="kieContainer")
public KieContainer kieContainer() {
return KieServices.Factory.get().getKieClasspathContainer();
}
}
Controller:
package com.test.drool.controllers;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
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.RestController;
import com.test.drool.facts.InterestRate;
import com.test.drool.service.RuleExecuteService;
#RestController
#RequestMapping(value="/rule")
public class RuleExecuteController {
#Autowired
private RuleExecuteService executeService;
private static Logger logger=LoggerFactory.getLogger(RuleExecuteController.class);
#PostMapping(value = "/execute", consumes = MediaType.APPLICATION_JSON_VALUE,produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<Double> getInterestRate(#RequestBody InterestRate interestRate){
logger.info(String.format("logging Request Object %s",interestRate.toString()));
return new ResponseEntity<Double>(executeService.executeRule(interestRate),HttpStatus.OK);
}
}
RuleServiceImpl:
package com.test.drool.service.impl;
import org.kie.api.runtime.KieContainer;
import org.kie.api.runtime.KieSession;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.test.drool.facts.InterestRate;
import com.test.drool.service.RuleExecuteService;
#Service
public class RuleExecutorServiceImpl implements RuleExecuteService {
private KieContainer kieContainer;
private static org.slf4j.Logger logger=LoggerFactory.getLogger(RuleExecutorServiceImpl.class);
#Autowired
public RuleExecutorServiceImpl(KieContainer kieContainer) {
this.kieContainer=kieContainer;
}
#Override
public double executeRule(InterestRate interestRate) {
logger.info("firing up session and executing rules");
KieSession kieSession= kieContainer.newKieSession("ksession-rules");
logger.info("Printing object before inserting in session"+interestRate.toString());
kieSession.insert(interestRate);
kieSession.fireAllRules();
System.out.println("returning values from rule execution"+">>>"+interestRate.getRate());
return interestRate.getRate();
}
}
DRL file:
package com.test.drool.facts
rule "Interest Rate"
when
$interestrate := InterestRate(lender.equals("RBL"))
then
$interestrate.setRate(12.30);
end
Gradle dependency:
dependencies {
compile "org.kie:kie-spring:${droolsVersion}"
compile "org.kie:kie-api:${droolsVersion}"
compile "org.drools:drools-core:${droolsVersion}"
compile "org.drools:drools-compiler:${droolsVersion}"
//compile('org.springframework.cloud:spring-cloud-starter-eureka')
compile('org.springframework.boot:spring-boot-starter-actuator')
compile ('org.springframework.boot:spring-boot-starter-web')
compile ('org.springframework.boot:spring-boot-starter-data-rest')
compile ('org.springframework.boot:spring-boot-devtools')
testCompile('org.springframework.boot:spring-boot-starter-test')
compile group: 'org.apache.httpcomponents', name: 'httpclient', version: '4.5.2'
compile group: 'org.apache.commons', name: 'commons-lang3', version: '3.0'
}
I don't know the reason why it works but we kept minimum spring drool in our gradle file.All me and my colleague did was to have only kie-spring and drool-compiler in gradle and it worked like a charm.We also need to exclude some modules of spring since spring boot 1.5 does not work with spring 3.2 on which kie depends.For future spring boot drool development,here is gradle. Just paste it and build and code as you will code for any other spring-boot project.
repositories {
mavenCentral()
maven {
name 'jboss'
url 'http://repository.jboss.org/nexus/content/groups/public-jboss'
}
}
buildscript {
ext {
springBootVersion = '1.5.3.RELEASE'
}
repositories {
mavenCentral()
}
dependencies {
classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
}
}
apply plugin: 'java'
apply plugin: 'eclipse'
apply plugin: 'org.springframework.boot'
version = '0.0.1'
sourceCompatibility = 1.8
targetCompatibility = 1.8
repositories {
mavenCentral()
}
dependencyManagement {
imports {
mavenBom 'org.springframework.cloud:spring-cloud-dependencies:Camden.SR6'
}
}
dependencies {
compile ('org.kie:kie-spring:6.5.0.Final'){
exclude group:'org.springframework', module: 'spring-core'
exclude group:'org.springframework', module: 'spring-tx'
exclude group:'org.springframework', module: 'spring-beans'
exclude group:'org.springframework', module: 'spring-context'
}
compile "org.drools:drools-compiler:6.5.0.Final"
compile ('org.springframework.boot:spring-boot-starter-web')
compile group: 'org.apache.commons', name: 'commons-lang3', version: '3.0'
}
task createFolder{
def configDir = new File(project.buildDir.path+"/libs", "config")
def keystoreDir = new File(project.buildDir.path+"/libs", "keystore")
def logDir = new File(project.buildDir.path+"/libs", "log")
def libDir = new File(project.buildDir.path+"/libs", "lib")
if(!logDir.exists()){
logDir.mkdirs()
}
delete configDir
delete libDir
delete keystoreDir
libDir.mkdirs()
configDir.mkdirs()
keystoreDir.mkdirs()
}
//copy config
task copyConfig(type: Copy) {
into project.buildDir.path+"/libs/config"
from "config"
}
//copy keystore
task copyKeystore(type: Copy) {
into project.buildDir.path+"/libs/keystore"
from "keystore"
}
//copy dependencies
task copyRuntimeLibs(type: Copy) {
into project.buildDir.path+"/libs/lib"
from configurations.compile
}
task bundleAll(type: Jar){
dependsOn 'createFolder', 'copyRuntimeLibs', 'copyConfig', 'copyKeystore'
manifest {
def manifestClasspath = configurations.compile.collect { "lib/" + it.getName() }.join(' ')
attributes 'Implementation-Title': 'rule-service',
'Implementation-Version': version,
'Main-Class': 'com.test.drool.starte.RuleStarter',
'Class-Path': manifestClasspath
}
baseName=project.name
from { (configurations.compile - configurations.compile).collect { it.isDirectory() ? it : zipTree(it) } }
with jar
exclude 'META-INF/*.RSA', 'META-INF/*.SF','META-INF/*.DSA'
}
task zip(type: Zip){
dependsOn 'bundleAll'
from 'build/libs'
}