Monday, July 31, 2017

Using Quartz for scheduling with MongoDB

I am sure most of us have used the Quartz library to handle scheduled activity within our projects. Although I have interacted with the library quite often in the past, it was the first time I had to use Quartz with MongoDB.

By default, Quartz only provides support for the traditional relational databases. Browsing through, I stumbled upon this github repository by Michael Klishin which provides a MongoDB implementation of the Quartz library in a clustered environment.

We will be using a Spring boot application to show you how we can integrate the Quartz library for scheduling in a clustered environment using MongoDB.

The GitHub repository with the code shown in this article can be found here.

All quartz related configuration is stored in a property file. The attributes we will be using are as follows;


 
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~
# Quartz Job Scheduling
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~


# Use the MongoDB store
org.quartz.jobStore.class=com.quartz.mongo.intro.quartzintro.scheduler.CustomMongoQuartzSchedulerJobStore


# --- # Note that all the mongo db configuration are set in the CustomMongoQuartzSchedulerJobStore.java class ---
# MongoDB URI (optional if 'org.quartz.jobStore.addresses' is set)
#org.quartz.jobStore.mongoUri=mongodb://localhost:27017

# Comma separated list of mongodb hosts/replica set seeds (optional if 'org.quartz.jobStore.mongoUri' is set)
#org.quartz.jobStore.addresses=localhost


# Will be used to create collections like quartz_jobs, quartz_triggers, quartz_calendars, quartz_locks
org.quartz.jobStore.collectionPrefix=quartz_

# Thread count setting is ignored by the MongoDB store but Quartz requires it
org.quartz.threadPool.threadCount=1

# Skip running a web request to determine if there is an updated version of Quartz available for download
org.quartz.scheduler.skipUpdateCheck=true

org.quartz.jobStore.isClustered=true

#The instance ID will be auto generated by Quartz for all nodes running in a cluster.
org.quartz.scheduler.instanceId=AUTO

org.quartz.scheduler.instanceName=quartzMongoInstance


Let us look at some of these properties. Others are self-explanatory with the comments provided.


  • org.quartz.jobStore.class : This defines the job store class which will handle storing the job related details in the database. By default, with the GitHub project mentioned before, we are provided with the MongoDBJobStore. For the purposes of this article however, we will extend the functionality provided by this class with our own implementation which will handle the MongoDB configuration based on Spring profiles.

  • org.quartz.jobStore.mongoUri : You will define the comma separated MongoDB URI's here if you wanted to use the default MongoDBJobStore class. On this implementation however, since we are defining a custom job store, we will not be using this property. An example of how you would define this would be mongodb://<ip1>:<port>,<ip2>:<port>

  • org.quartz.jobStore.collectionPrefix : This property defines the prefix for the collections created for the purposes of storing quartz specific details.


Let us first see how our JobStore configuration class looks like;


 
package com.quartz.mongo.intro.quartzintro.scheduler;

import org.apache.commons.lang3.StringUtils;
import org.quartz.impl.StdSchedulerFactory;
import org.springframework.beans.factory.config.YamlPropertiesFactoryBean;
import org.springframework.core.io.ClassPathResource;

import com.novemberain.quartz.mongodb.MongoDBJobStore;
import com.quartz.mongo.intro.quartzintro.constants.SchedulerConstants;
import com.quartz.mongo.intro.quartzintro.constants.SystemProperties;

/**
 * 
 * <p>
 * We extend the {@link MongoDBJobStore} because we need to set the custom mongo
 * db parameters. Some of the configuration comes from system properties set via
 * docker and the others come via the application.yml files we have for each
 * environment.
 * </p>
 * 
 * < These are set as part of initialization. This class is initialized by
 * {@link StdSchedulerFactory} and defined in the quartz.properties file.
 * 
 * </p>
 * 
 * @author dinuka
 *
 */
public class CustomMongoQuartzSchedulerJobStore extends MongoDBJobStore {

 private static String mongoAddresses;
 private static String userName;
 private static String password;
 private static String dbName;
 private static boolean isSSLEnabled;
 private static boolean isSSLInvalidHostnameAllowed;

 public CustomMongoQuartzSchedulerJobStore() {
  super();
  initializeMongo();
  setMongoUri("mongodb://" + mongoAddresses);
  setUsername(userName);
  setPassword(password);
  setDbName(dbName);
  setMongoOptionEnableSSL(isSSLEnabled);
  setMongoOptionSslInvalidHostNameAllowed(isSSLInvalidHostnameAllowed);
 }

 /**
  * <p>
  * This method will initialize the mongo instance required by the Quartz
  * scheduler.
  * 
  * The use case here is that we have two profiles;
  * </p>
  * 
  * <ul>
  * <li>Development</li>
  * <li>Production</li>
  * </ul>
  * 
  * <p>
  * So when constructing the mongo instance to be used for the Quartz
  * scheduler, we need to read the various properties set within the system
  * to determine which would be appropriate depending on which spring profile
  * is active.
  * </p>
  * 
  */
 private static void initializeMongo() {
  /**
   * The use case here is that when we run our application, the property
   * spring.profiles.active is set as a system property during production.
   * But it will not be set in a development environment.
   */
  String env = System.getProperty(SystemProperties.ENVIRONMENT);
  env = StringUtils.isNotBlank(env) ? env : "dev";
  YamlPropertiesFactoryBean commonProperties = new YamlPropertiesFactoryBean();
  commonProperties.setResources(new ClassPathResource("application.yml"));
  /**
   * The mongo DB user name and password are only password as command line
   * parameters in the production environment and for the development
   * environment it will be null which is why we use
   * StringUtils#trimToEmpty so we can pass empty strings for the user
   * name and password in the development environment since we do not have
   * authentication on the development environment.s
   */
  userName = StringUtils.trimToEmpty(commonProperties.getObject().getProperty(SystemProperties.SERVER_NAME));
  password = StringUtils.trimToEmpty(System.getProperty(SystemProperties.MONGO_PASSWORD));
  dbName = commonProperties.getObject().getProperty(SchedulerConstants.QUARTZ_SCHEDULER_DB_NAME);

  YamlPropertiesFactoryBean environmentSpecificProperties = new YamlPropertiesFactoryBean();

  userName = commonProperties.getObject().getProperty(SystemProperties.SERVER_NAME);

  switch (env) {
  case "prod":
   environmentSpecificProperties.setResources(new ClassPathResource("application-prod.yml"));
   /**
    * By deafult, in the production mongo instance, SSL is enabled and
    * SSL invalid host name allowed property is set.
    */
   isSSLEnabled = true;
   isSSLInvalidHostnameAllowed = true;
   mongoAddresses = environmentSpecificProperties.getObject().getProperty(SystemProperties.MONGO_URI);
   break;
  case "dev":
   /**
    * For the development profile, we just read the mongo URI that is
    * set.
    */
   environmentSpecificProperties.setResources(new ClassPathResource("application-dev.yml"));
   mongoAddresses = environmentSpecificProperties.getObject().getProperty(SystemProperties.MONGO_URI);
   break;

  }

 }

}


In this above implementation, we have retrieved the MongoDB details pertaining to the active profile. If no profile is defined it defaults to the development profile. We have used the YamlPropertiesFactoryBean here to read off the application properties pertaining to different environments.

Moving on, we then need to let Spring manage the creation of the Quartz configuration using the SchedulerFactoryBean


 
package com.quartz.mongo.intro.quartzintro.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;
import org.springframework.scheduling.quartz.SchedulerFactoryBean;

/**
 * This class will configure and setup quartz using the
 * {@link SchedulerFactoryBean}
 * 
 * @author dinuka
 *
 */
@Configuration
public class QuartzConfiguration {

 /**
  * Here we integrate quartz with Spring and let Spring manage initializing
  * quartz as a spring bean.
  * 
  * @return an instance of {@link SchedulerFactoryBean} which will be managed
  *         by spring.
  */
 @Bean
 public SchedulerFactoryBean schedulerFactoryBean() {
  SchedulerFactoryBean scheduler = new SchedulerFactoryBean();
  scheduler.setApplicationContextSchedulerContextKey("applicationContext");
  scheduler.setConfigLocation(new ClassPathResource("quartz.properties"));
  scheduler.setWaitForJobsToCompleteOnShutdown(true);
  return scheduler;
 }

}

We define this as a Configuration class so that it will be picked up when we run the Spring boot application.

The call to setApplicationContextSchedulerContextKey method here is in order to get a reference to the Spring application context within our job class which is as follows;


 
package com.quartz.mongo.intro.quartzintro.scheduler.jobs;

import org.quartz.DisallowConcurrentExecution;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import org.quartz.PersistJobDataAfterExecution;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.core.env.Environment;
import org.springframework.scheduling.quartz.QuartzJobBean;
import org.springframework.scheduling.quartz.SchedulerFactoryBean;

import com.quartz.mongo.intro.quartzintro.config.JobConfiguration;
import com.quartz.mongo.intro.quartzintro.config.QuartzConfiguration;

/**
 * 
 * This is the job class that will be triggered based on the job configuration
 * defined in {@link JobConfiguration}
 * 
 * @author dinuka
 *
 */
@PersistJobDataAfterExecution
@DisallowConcurrentExecution
public class SampleJob extends QuartzJobBean {

 private static Logger log = LoggerFactory.getLogger(SampleJob.class);

 private ApplicationContext applicationContext;

 /**
  * This method is called by Spring since we set the
  * {@link SchedulerFactoryBean#setApplicationContextSchedulerContextKey(String)}
  * in {@link QuartzConfiguration}
  * 
  * @param applicationContext
  */
 public void setApplicationContext(ApplicationContext applicationContext) {
  this.applicationContext = applicationContext;
 }

 /**
  * This is the method that will be executed each time the trigger is fired.
  */
 @Override
 protected void executeInternal(JobExecutionContext context) throws JobExecutionException {
  log.info("This is the sample job, executed by {}", applicationContext.getBean(Environment.class));

 }
}


As you can see, we get a reference to the application context when the SchedulerFactoryBean is initialised. The part of the Spring documentation I would like to draw you attention to is as follows;

In case of a QuartzJobBean, the reference will be applied to the Job
instance as bean property. An "applicationContext" attribute will
correspond to a "setApplicationContext" method in that scenario.



Next up, we go on to configure the job to be run with the frequency by which to run the scheduled activity.



 
package com.quartz.mongo.intro.quartzintro.config;

import static org.quartz.TriggerBuilder.newTrigger;

import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.Date;

import javax.annotation.PostConstruct;

import org.quartz.JobDetail;
import org.quartz.JobKey;
import org.quartz.SimpleScheduleBuilder;
import org.quartz.Trigger;
import org.quartz.TriggerKey;
import org.quartz.impl.JobDetailImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.quartz.SchedulerFactoryBean;

import com.quartz.mongo.intro.quartzintro.constants.SchedulerConstants;
import com.quartz.mongo.intro.quartzintro.scheduler.jobs.SampleJob;

/**
 * 
 * This will configure the job to run within quartz.
 * 
 * @author dinuka
 *
 */
@Configuration
public class JobConfiguration {

 @Autowired
 private SchedulerFactoryBean schedulerFactoryBean;

 @PostConstruct
 private void initialize() throws Exception {
  schedulerFactoryBean.getScheduler().addJob(sampleJobDetail(), true, true);
  if (!schedulerFactoryBean.getScheduler().checkExists(new TriggerKey(
    SchedulerConstants.SAMPLE_JOB_POLLING_TRIGGER_KEY, SchedulerConstants.SAMPLE_JOB_POLLING_GROUP))) {
   schedulerFactoryBean.getScheduler().scheduleJob(sampleJobTrigger());
  }

 }

 /**
  * <p>
  * The job is configured here where we provide the job class to be run on
  * each invocation. We give the job a name and a value so that we can
  * provide the trigger to it on our method {@link #sampleJobTrigger()}
  * </p>
  * 
  * @return an instance of {@link JobDetail}
  */
 private static JobDetail sampleJobDetail() {
  JobDetailImpl jobDetail = new JobDetailImpl();
  jobDetail.setKey(
    new JobKey(SchedulerConstants.SAMPLE_JOB_POLLING_JOB_KEY, SchedulerConstants.SAMPLE_JOB_POLLING_GROUP));
  jobDetail.setJobClass(SampleJob.class);
  jobDetail.setDurability(true);
  return jobDetail;
 }

 /**
  * <p>
  * This method will define the frequency with which we will be running the
  * scheduled job which in this instance is every minute three seconds after
  * the start up.
  * </p>
  * 
  * @return an instance of {@link Trigger}
  */
 private static Trigger sampleJobTrigger() {
  return newTrigger().forJob(sampleJobDetail())
    .withIdentity(SchedulerConstants.SAMPLE_JOB_POLLING_TRIGGER_KEY,
      SchedulerConstants.SAMPLE_JOB_POLLING_GROUP)
    .withPriority(50).withSchedule(SimpleScheduleBuilder.repeatMinutelyForever())
    .startAt(Date.from(LocalDateTime.now().plusSeconds(3).atZone(ZoneId.systemDefault()).toInstant()))
    .build();
 }

}


There are many ways you can configure your scheduler including cron configuration. For the purposes of this article, we will define a simple trigger to run every minute, three seconds after start up. We define this as a Configuration class so that it will be picked up when we run the Spring boot application.


That is about it. When you now run the Spring Boot application class found in the GitHub repository with a running MongoDB instance, you will see the following collections created;


  • quartz_calendars
  • quartz_jobs
  • quartz_locks
  • quartz_schedulers
  • quartz_triggers

Thank you for reading and if there are any comments, improvements, suggestions, do kindly leave by a comment which is always appreciated.




Friday, July 28, 2017

Spring Boot with the Justice League

Dark times are ahead for the Justice League with the formidable Darkseid coming over to conquer human kind. Batman with the help of Wonder woman are on a quest to get the league together with one critical aspect missing. A proper Justice league member management system. As time is not on their side, they do not want to go through the cumbersome process of setting up a project from scratch with all the things they need. Batman hands over this daunting task of building a rapid system to his beloved trusted Alfred (As robin is so unpredictable) who tells Batman that he recalls coming across something called Spring Boot which helps set up everything you need so you can get to writing code for your application rather than being bogged down with minor nuances of setting up configuration for your project. And so he gets into it. Let's get onto it with our beloved Alfred who will utilize Spring Boot to build a Justice League member management system in no time. Well at least the back-end part for now since Batman like dealing directly with the REST APIs.

There are many convenient ways of setting up a Spring Boot application. For this article, we will focus on the traditional way of downloading the package (Spring CLI) and setting it up from scratch on Ubuntu. Spring also supports getting a project packaged on-line via their tool. You can download the latest stable release from here. For this post, I am using the 1.3.0.M1 release.

After extracting your downloaded archive, first off, set the following parameters on your profile;


 
SPRING_BOOT_HOME=<extracted path>/spring-1.3.0.M1

PATH=$SPRING_BOOT_HOME/bin:$PATH


Afterwards in your "bashrc" file, include the following;


 
. <extracted-path>/spring-1.3.0.M1/shell-completion/bash/spring


What that last execution does is it gives you auto completion on the command line when you are dealing with the spring-cli to create your spring boot applications. Please remember to "source" both the profile and the "bashrc" files for the changes to take affect.

Our technology stack which is used in this article will be as follows;
  • Spring REST
  • Spring Data
  • MongoDB

So let us start off creating the template project for the application by issuing the following command. Note that the sample project can be downloaded from by GitHub repository found here;


 
spring init -dweb,data-mongodb,flapdoodle-mongo  --groupId com.justiceleague --artifactId justiceleaguemodule --build maven justiceleaguesystem

This will generate a maven project with Spring MVC and Spring Data with an emebedded MongoDB.

By default, the spring-cli creates a project with the name set as "Demo". So we will need to rename the respective application class generated. If you checked out the source from my GitHub repository mentioned above then this will be done.

With Spring boot, running the application is as easy as running the jar file created by the project which essentially calls onto the application  class annotated with @SpringBootApplication that boots up Spring. Let us see how that looks like;




 
package com.justiceleague.justiceleaguemodule;

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

/**
 * The main spring boot application which will start up a web container and wire
 * up all the required beans.
 * 
 * @author dinuka
 *
 */
@SpringBootApplication
public class JusticeLeagueManagementApplication {

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


We then move onto our domain classes where we use spring-data along with mongodb to define our data layer. The domain class is as follows;


 
package com.justiceleague.justiceleaguemodule.domain;

import org.bson.types.ObjectId;
import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.index.Indexed;
import org.springframework.data.mongodb.core.mapping.Document;

/**
 * This class holds the details that will be stored about the justice league
 * members on MongoDB.
 * 
 * @author dinuka
 *
 */
@Document(collection = "justiceLeagueMembers")
public class JusticeLeagueMemberDetail {

 @Id
 private ObjectId id;

 @Indexed
 private String name;

 private String superPower;

 private String location;

 public JusticeLeagueMemberDetail(String name, String superPower, String location) {
  this.name = name;
  this.superPower = superPower;
  this.location = location;
 }

 public String getId() {
  return id.toString();
 }

 public void setId(String id) {
  this.id = new ObjectId(id);
 }

 public String getName() {
  return name;
 }

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

 public String getSuperPower() {
  return superPower;
 }

 public void setSuperPower(String superPower) {
  this.superPower = superPower;
 }

 public String getLocation() {
  return location;
 }

 public void setLocation(String location) {
  this.location = location;
 }

}

As we are using spring-data, it is fairly intuitive, specially if you are coming from a JPA/Hibernate background. The annotations are very similar. The only new thing would be the @Document annotation which denotes the name of the collection in our mongo database. We also have an index defined on the name of the super hero since more queries will revolve around searching by the name.

With Spring-data came the functionality of defining your repositories easily that support the usual CRUD operations and some read operations straight out of the box without you having to write them. So we utilise the power of Spring-data repositories in our application as well and the repository class is as follows;



 
package com.justiceleague.justiceleaguemodule.dao;

import org.springframework.data.mongodb.repository.MongoRepository;
import org.springframework.data.mongodb.repository.Query;

import com.justiceleague.justiceleaguemodule.domain.JusticeLeagueMemberDetail;

public interface JusticeLeagueRepository extends MongoRepository<JusticeLeagueMemberDetail, String> {

 /**
  * This method will retrieve the justice league member details pertaining to
  * the name passed in.
  * 
  * @param superHeroName
  *            the name of the justice league member to search and retrieve.
  * @return an instance of {@link JusticeLeagueMemberDetail} with the member
  *         details.
  */
 @Query("{ 'name' : {$regex: ?0, $options: 'i' }}")
 JusticeLeagueMemberDetail findBySuperHeroName(final String superHeroName);
}


The usual saving operations are implemented by Spring at runtime through the use of proxies and we just have to define our domain class in our repository.

As you can see, we have only one method defined. With the @Query annotation, we are trying to find a super hero with the user of regular expressions. The options "i" denotes that we should ignore case when trying to find a match in mongo db.

Next up,  we move onto implementing our logic to storing the new justice league members through our service layer.


 
package com.justiceleague.justiceleaguemodule.service.impl;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import com.justiceleague.justiceleaguemodule.constants.MessageConstants.ErrorMessages;
import com.justiceleague.justiceleaguemodule.dao.JusticeLeagueRepository;
import com.justiceleague.justiceleaguemodule.domain.JusticeLeagueMemberDetail;
import com.justiceleague.justiceleaguemodule.exception.JusticeLeagueManagementException;
import com.justiceleague.justiceleaguemodule.service.JusticeLeagueMemberService;
import com.justiceleague.justiceleaguemodule.web.dto.JusticeLeagueMemberDTO;
import com.justiceleague.justiceleaguemodule.web.transformer.DTOToDomainTransformer;

/**
 * This service class implements the {@link JusticeLeagueMemberService} to
 * provide the functionality required for the justice league system.
 * 
 * @author dinuka
 *
 */
@Service
public class JusticeLeagueMemberServiceImpl implements JusticeLeagueMemberService {

 @Autowired
 private JusticeLeagueRepository justiceLeagueRepo;

 /**
  * {@inheritDoc}
  */
 public void addMember(JusticeLeagueMemberDTO justiceLeagueMember) {
  JusticeLeagueMemberDetail dbMember = justiceLeagueRepo.findBySuperHeroName(justiceLeagueMember.getName());

  if (dbMember != null) {
   throw new JusticeLeagueManagementException(ErrorMessages.MEMBER_ALREDY_EXISTS);
  }
  JusticeLeagueMemberDetail memberToPersist = DTOToDomainTransformer.transform(justiceLeagueMember);
  justiceLeagueRepo.insert(memberToPersist);
 }

}


Again quite trivial, if the member already exists, we throw out an error, else we add the member. Here you can see we are using the already implemented insert method of the spring data repository we just defined before.

Finally Alfred is ready to expose the new functionality he just developed via a REST API using Spring REST so that Batman can start sending in the details over HTTP as he is always travelling.


package com.justiceleague.justiceleaguemodule.web.rest.controller;

import javax.validation.Valid;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;

import com.justiceleague.justiceleaguemodule.constants.MessageConstants;
import com.justiceleague.justiceleaguemodule.service.JusticeLeagueMemberService;
import com.justiceleague.justiceleaguemodule.web.dto.JusticeLeagueMemberDTO;
import com.justiceleague.justiceleaguemodule.web.dto.ResponseDTO;

/**
 * This class exposes the REST API for the system.
 * 
 * @author dinuka
 *
 */
@RestController
@RequestMapping("/justiceleague")
public class JusticeLeagueManagementController {

 @Autowired
 private JusticeLeagueMemberService memberService;

 /**
  * This method will be used to add justice league members to the system.
  * 
  * @param justiceLeagueMember
  *            the justice league member to add.
  * @return an instance of {@link ResponseDTO} which will notify whether
  *         adding the member was successful.
  */
 @ResponseBody
 @ResponseStatus(value = HttpStatus.CREATED)
 @RequestMapping(method = RequestMethod.POST, path = "/addMember", produces = {
   MediaType.APPLICATION_JSON_VALUE }, consumes = { MediaType.APPLICATION_JSON_VALUE })
 public ResponseDTO addJusticeLeagueMember(@Valid @RequestBody JusticeLeagueMemberDTO justiceLeagueMember) {
  ResponseDTO responseDTO = new ResponseDTO(ResponseDTO.Status.SUCCESS,
    MessageConstants.MEMBER_ADDED_SUCCESSFULLY);
  try {
   memberService.addMember(justiceLeagueMember);
  } catch (Exception e) {
   responseDTO.setStatus(ResponseDTO.Status.FAIL);
   responseDTO.setMessage(e.getMessage());
  }
  return responseDTO;
 }
}


We expose our functionality as a JSON payload as Batman just cannot get enough of it although Alfred is a bit old school and prefer XML sometimes.

The old guy Alfred still wants to test out his functionality as TDD is just his style. So finally we look at the integration tests written up by Alfred to make sure the initial version of the Justice league management system is working as expected. Note that we are only showing the REST API tests here although Alfred has actually covered more which you can check out on the GitHub repo.


 
package com.justiceleague.justiceleaguemodule.test.util;

import java.io.IOException;
import java.net.UnknownHostException;

import org.junit.After;
import org.junit.AfterClass;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.runner.RunWith;
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.data.mongodb.core.MongoTemplate;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.justiceleague.justiceleaguemodule.domain.JusticeLeagueMemberDetail;

import de.flapdoodle.embed.mongo.MongodExecutable;
import de.flapdoodle.embed.mongo.MongodStarter;
import de.flapdoodle.embed.mongo.config.IMongodConfig;
import de.flapdoodle.embed.mongo.config.MongodConfigBuilder;
import de.flapdoodle.embed.mongo.config.Net;
import de.flapdoodle.embed.mongo.distribution.Version;

/**
 * This class will have functionality required when running integration tests so
 * that invidivual classes do not need to implement the same functionality.
 * 
 * @author dinuka
 *
 */
@RunWith(SpringRunner.class)
@SpringBootTest
@AutoConfigureMockMvc
public abstract class BaseIntegrationTest {

 @Autowired
 protected MockMvc mockMvc;

 protected ObjectMapper mapper;

 private static MongodExecutable mongodExecutable;

 @Autowired
 protected MongoTemplate mongoTemplate;

 @Before
 public void setUp() {
  mapper = new ObjectMapper();
 }

 @After
 public void after() {
  mongoTemplate.dropCollection(JusticeLeagueMemberDetail.class);
 }

 /**
  * Here we are setting up an embedded mongodb instance to run with our
  * integration tests.
  * 
  * @throws UnknownHostException
  * @throws IOException
  */
 @BeforeClass
 public static void beforeClass() throws UnknownHostException, IOException {

  MongodStarter starter = MongodStarter.getDefaultInstance();

  IMongodConfig mongoConfig = new MongodConfigBuilder().version(Version.Main.PRODUCTION)
    .net(new Net(27017, false)).build();

  mongodExecutable = starter.prepare(mongoConfig);

  try {
   mongodExecutable.start();
  } catch (Exception e) {
   closeMongoExecutable();
  }
 }

 @AfterClass
 public static void afterClass() {
  closeMongoExecutable();
 }

 private static void closeMongoExecutable() {
  if (mongodExecutable != null) {
   mongodExecutable.stop();
  }
 }

}



 
package com.justiceleague.justiceleaguemodule.web.rest.controller;

import org.hamcrest.beans.SamePropertyValuesAs;
import org.junit.Assert;
import org.junit.Test;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import org.springframework.test.web.servlet.result.MockMvcResultMatchers;

import com.justiceleague.justiceleaguemodule.constants.MessageConstants;
import com.justiceleague.justiceleaguemodule.constants.MessageConstants.ErrorMessages;
import com.justiceleague.justiceleaguemodule.domain.JusticeLeagueMemberDetail;
import com.justiceleague.justiceleaguemodule.test.util.BaseIntegrationTest;
import com.justiceleague.justiceleaguemodule.web.dto.JusticeLeagueMemberDTO;
import com.justiceleague.justiceleaguemodule.web.dto.ResponseDTO;
import com.justiceleague.justiceleaguemodule.web.dto.ResponseDTO.Status;

/**
 * This class will test out the REST controller layer implemented by
 * {@link JusticeLeagueManagementController}
 * 
 * @author dinuka
 *
 */
public class JusticeLeagueManagementControllerTest extends BaseIntegrationTest {

 /**
  * This method will test if the justice league member is added successfully
  * when valid details are passed in.
  * 
  * @throws Exception
  */
 @Test
 public void testAddJusticeLeagueMember() throws Exception {

  JusticeLeagueMemberDTO flash = new JusticeLeagueMemberDTO("Barry Allen", "super speed", "Central City");
  String jsonContent = mapper.writeValueAsString(flash);
  String response = mockMvc
    .perform(MockMvcRequestBuilders.post("/justiceleague/addMember").accept(MediaType.APPLICATION_JSON)
      .contentType(MediaType.APPLICATION_JSON).content(jsonContent))
    .andExpect(MockMvcResultMatchers.status().isCreated()).andReturn().getResponse().getContentAsString();

  ResponseDTO expected = new ResponseDTO(Status.SUCCESS, MessageConstants.MEMBER_ADDED_SUCCESSFULLY);
  ResponseDTO receivedResponse = mapper.readValue(response, ResponseDTO.class);

  Assert.assertThat(receivedResponse, SamePropertyValuesAs.samePropertyValuesAs(expected));

 }

 /**
  * This method will test if an appropriate failure response is given when
  * the member being added already exists within the system.
  * 
  * @throws Exception
  */
 @Test
 public void testAddJusticeLeagueMemberWhenMemberAlreadyExists() throws Exception {
  JusticeLeagueMemberDetail flashDetail = new JusticeLeagueMemberDetail("Barry Allen", "super speed",
    "Central City");
  mongoTemplate.save(flashDetail);

  JusticeLeagueMemberDTO flash = new JusticeLeagueMemberDTO("Barry Allen", "super speed", "Central City");
  String jsonContent = mapper.writeValueAsString(flash);
  String response = mockMvc
    .perform(MockMvcRequestBuilders.post("/justiceleague/addMember").accept(MediaType.APPLICATION_JSON)
      .contentType(MediaType.APPLICATION_JSON).content(jsonContent))
    .andExpect(MockMvcResultMatchers.status().isCreated()).andReturn().getResponse().getContentAsString();

  ResponseDTO expected = new ResponseDTO(Status.FAIL, ErrorMessages.MEMBER_ALREDY_EXISTS);
  ResponseDTO receivedResponse = mapper.readValue(response, ResponseDTO.class);
  Assert.assertThat(receivedResponse, SamePropertyValuesAs.samePropertyValuesAs(expected));
 }

 /**
  * This method will test if a valid client error is given if the data
  * required are not passed within the JSON request payload which in this
  * case is the super hero name.
  * 
  * @throws Exception
  */
 @Test
 public void testAddJusticeLeagueMemberWhenNameNotPassedIn() throws Exception {
  // The super hero name is passed in as null here to see whether the
  // validation error handling kicks in.
  JusticeLeagueMemberDTO flash = new JusticeLeagueMemberDTO(null, "super speed", "Central City");
  String jsonContent = mapper.writeValueAsString(flash);
  mockMvc.perform(MockMvcRequestBuilders.post("/justiceleague/addMember").accept(MediaType.APPLICATION_JSON)
    .contentType(MediaType.APPLICATION_JSON).content(jsonContent))
    .andExpect(MockMvcResultMatchers.status().is4xxClientError());

 }

}



And that is about it. With the power of Spring boot, Alfred was able to get a bare minimum Justice league management system with a REST API exposed in no time. We will build upon this application in the time to come and see how Alfred comes up with getting this application deployed via docker to an Amazon AWS instance managed by Kubernetes in the time to come. Exciting times ahead so tune in.