My Works

To refer to Spring Boot version of the same Application follow SpringBootRestService.

Rest Service with Spring MVC.

Here we are going to look at creating a Rest service using Spring MVC.

The RestService that we are going to create would implement a logic for coin machine which gives change based on the denominations of the coins provided or available.

  1. Create Project.
  2. Create Application Config.
  3. Create WebConfig.
  4. Create Calculation Implementation.
  5. Create Service Layer for the Calculation.
  6. Create ChangeController.
  7. Deploy and Run the Rest service.
  8. Download the example.

1.Create RestServiceSpringMVC project.

Create a RestServiceSpringMVC Project folder. Add a POM file with Project dependencies consisting of JDK 1.8,mockito-all,powermock-module-junit4, powermock-api-mockito,javax.servlet-api,spring-webmvc,spring-expression,spring-test,spring-tx,jackson-core-asl,jackson-mapper-asl, jackson-annotations,jackson-core,jackson-databind and jackson-dataformat-xml.The common, logging and junit jars also needs to be added. The pom.xml is available in the downloaded example.The pom file dependencies are listed below.


<properties>
<log4j.version>1.2.17</log4j.version>
<spring.version>4.2.1.RELEASE</spring.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>

<dependencies>

<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-all</artifactId>
<version>1.9.0</version>
<scope>test</scope>
</dependency>

<dependency>
<groupId>org.powermock</groupId>
<artifactId>powermock-module-junit4</artifactId>
<version>1.6.6</version>
<scope>test</scope>
</dependency>

<dependency>
<groupId>org.powermock</groupId>
<artifactId>powermock-api-mockito</artifactId>
<version>1.6.6</version>
<scope>test</scope>
</dependency>

<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>3.0.1</version>
<scope>provided</scope>
</dependency>

<dependency>
<groupId>org.slf4j</groupId>
<artifactId>jcl-over-slf4j</artifactId>
<version>1.7.0</version>
</dependency>

<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.7.0</version>
</dependency>

<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>1.7.0</version>
</dependency>

<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>${spring.version}</version>
</dependency>

<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-expression</artifactId>
<version>${spring.version}</version>
</dependency>

<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>${spring.version}</version>
</dependency>

<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
</dependency>


<dependency>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
<version>1.1.1</version>
</dependency>

<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-tx</artifactId>
<version>${spring.version}</version>
</dependency>

<dependency>
<groupId>org.codehaus.jackson</groupId>
<artifactId>jackson-core-asl</artifactId>
<version>1.9.11</version>
</dependency>

<dependency>
<groupId>org.codehaus.jackson</groupId>
<artifactId>jackson-mapper-asl</artifactId>
<version>1.9.11</version>
</dependency>

<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-annotations</artifactId>
<version>2.7.2</version>
</dependency>

<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
<version>2.7.2</version>
</dependency>

<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.7.2</version>
</dependency>

<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-xml</artifactId>
<version>2.7.2</version>
</dependency>

</dependencies>


2.Create Application Config.

Application Config declares @Bean methods for Spring container to generate bean definitions and service requests that are required at runtime.Please Refer this link for some more information on annotation Configuration.


@Configuration
@ComponentScan(basePackages = { "org.srinivas.siteworks" }, excludeFilters={@Filter(type=FilterType.ANNOTATION,value=EnableWebMvc.class)})
public class AppConfig {

@Bean
public static PropertySourcesPlaceholderConfigurer placeHolderConfigurer() {
PropertySourcesPlaceholderConfigurer c = new PropertySourcesPlaceholderConfigurer();
c.setIgnoreUnresolvablePlaceholders(true);
return c;
}
}


3.Create WebConfig.

Configure the MVC context programmatically by extending WebMvcConfigurerAdapter and implementing WebApplicationInitializer.


@Configuration
@EnableWebMvc
@ComponentScan(basePackages = { "org.srinivas.siteworks" })
public class ChangeMvcContextConfiguration extends WebMvcConfigurerAdapter {
@Override
public void configureDefaultServletHandling(final DefaultServletHandlerConfigurer configurer) {
configurer.enable();
}
}




@Configuration
public class ChangeViewConfiguration {
private static final Logger logger = LoggerFactory.getLogger(ChangeViewConfiguration.class);

@Bean
public ViewResolver viewResolver() {
logger.info("ChangeViewConfiguration viewResolver()");
return new ContentNegotiatingViewResolver();
}
}




public class ChangeWebInitializer implements WebApplicationInitializer {
private static final Logger logger = LoggerFactory.getLogger(ChangeWebInitializer.class);

@Override
public void onStartup(ServletContext container) {
logger.info("Started to pickup the annotated classes at ChangeWebInitializer");
startServlet(container);
}

private void startServlet(final ServletContext container) {
WebApplicationContext dispatcherContext = registerContext(ChangeMvcContextConfiguration.class);
DispatcherServlet dispatcherServlet = new DispatcherServlet(dispatcherContext);
container.addListener(new ContextLoaderListener(dispatcherContext));
ServletRegistration.Dynamic dispatcher;
dispatcher = container.addServlet("dispatcher", dispatcherServlet);
dispatcher.setLoadOnStartup(1);
dispatcher.addMapping("/");
}

private WebApplicationContext registerContext(final Class<?>... annotatedClasses) {
logger.info("Using AnnotationConfigWebApplicationContext createContext");
AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext();
context.register(annotatedClasses);
return context;
}
}


4.Create Calculation Implementation.



public interface Calculate {

public List<Coin> calculate(Integer pence,Integer[] denominations)throws Exception;

}



public class CalculateException extends Exception {

private static final long serialVersionUID = 1L;

public CalculateException(String message,Throwable t){
super(message,t);
}

public CalculateException(String message){
super(message);
}
}



public class Coin {
private String name;
private Integer value;
private Integer count;

public Integer getValue() {
return value;
}

public void setValue(Integer value) {
this.value = value;
}

public Integer getCount() {
return count;
}

public void setCount(Integer count) {
this.count = count;
}

public String getName() {
return name;
}

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



@Component
public class OptimalCalculator implements Calculate {

public static final Logger log = LoggerFactory.getLogger(SupplyCalculator.class);

@Override
public List<Coin> calculate(Integer pence, Integer[] denominations) {
List<Coin> coinsMap = new ArrayList<Coin>();
try {
AtomicInteger remainingPence = new AtomicInteger(pence);
optimalCalculation(denominations, coinsMap, remainingPence);
} catch (Exception e) {
log.error("OptimalCalculation Unsuccessful", e);
}
return coinsMap;
}

private void optimalCalculation(Integer[] denominations, List<Coin> coinsMap, AtomicInteger remainingPence) {
Arrays.stream(denominations).forEach(denomination -> {
if (remainingPence.get() > 0) {
denominationCalculation(remainingPence.get(), coinsMap, denomination);
remainingPence.set(remainingCalculation(remainingPence.get(), denomination));
}
});
}

private void denominationCalculation(Integer pence, List<Coin> coinsMap, Integer denomination) {
Integer coins = Math.floorDiv(pence, denomination);
Coin coin = new Coin();
coin.setValue(denomination);
coin.setCount(coins);
addToCoinsList(coinsMap, coins, coin);
}

private void addToCoinsList(List<Coin> coinsMap, Integer coins, Coin coin) {
if (coins > 0) {
coinsMap.add(coin);
}
}

private Integer remainingCalculation(Integer pence, Integer denomination) {
Integer remainingPence = pence % denomination;
return remainingPence;
}
}



@Component
public class SupplyCalculator implements Calculate {
@Autowired
PropertiesReadWriter propertiesReadWriter;
null
public static final Logger log = LoggerFactory.getLogger(SupplyCalculator.class);

public PropertiesReadWriter getPropertiesReadWriter() {
return propertiesReadWriter;
}

public void setPropertiesReadWriter(PropertiesReadWriter propertiesReadWriter) {
this.propertiesReadWriter = propertiesReadWriter;
}

@Override
public List<Coin> calculate(Integer pence, Integer[] denominations) throws Exception {
List<Coin> suppliedCoins = new ArrayList<Coin>();
try {
Map<Integer, Integer> inventoryMap = propertiesReadWriter.getInventoryMap();
Integer inventoryTotal = inventoryMapValueTotal(inventoryMap);
List<Integer> shortSupplyList = new ArrayList<Integer>();
AtomicInteger remaining = new AtomicInteger(pence);
if(inventoryTotal >= pence){
supplyCalculation(denominations, inventoryMap, shortSupplyList, remaining, suppliedCoins);
propertiesReadWriter.writeInventoryData(inventoryMap);
}
} catch (Exception e) {
suppliedCoins = null;
suppliedCoins = new ArrayList<Coin>();
log.error("SupplyCalculation Unsuccessful",e);
}
return suppliedCoins;
}

private void supplyCalculation(Integer[] denominations, Map<Integer, Integer> inventoryMap, List<Integer> shortSupplyList, AtomicInteger remaining, List<Coin> suppliedCoins) throws Exception {
OptimalCalculator optimalCalculator = new OptimalCalculator();
if(denominations.length == 0){
throw new CalculateException("Insufficient Supply of Coins");
}
List<Coin> optimalCalculatedCoins = optimalCalculator.calculate(remaining.get(), denominations);
calculateSupplyCoins(denominations, inventoryMap, shortSupplyList, remaining, suppliedCoins, optimalCalculatedCoins);
}

private void calculateSupplyCoins(Integer[] denominations, Map<Integer, Integer> inventoryMap, List<Integer> shortSupplyList, AtomicInteger remaining, List<Coin> suppliedCoins, List<Coin> optimalCalculatedCoins)throws Exception{
Arrays.stream(denominations).forEach(key -> {
if(remaining.get() > 0 && denominations.length > 0 && isOneofOptimalValue(optimalCalculatedCoins,key)){

try {
Coin coin = filterByValue(optimalCalculatedCoins, key);
if (coin != null && coin.getCount() > inventoryMap.get(key)) {
insufficientInventoryChanges(inventoryMap, shortSupplyList, remaining, suppliedCoins, key);
} else {
inventoryAvailableChanges(inventoryMap, remaining, suppliedCoins, key, coin);
}
Integer[] denom = evaluateDenom(denominations, shortSupplyList, remaining);
recurseSupplyCalculation(inventoryMap, shortSupplyList, remaining, suppliedCoins, denom);
} catch (Exception e) {
throw new RuntimeException("Insufficient Supply of Coins",e);
}
}
});
}

private Integer[] evaluateDenom(Integer[] denominations, List<Integer> shortSupplyList, AtomicInteger remaining) {
Integer[] denom = new Integer[]{};
if(remaining.get() > 0 && shortSupplyList.size() > 0){
denom = Arrays.stream(denominations).filter(den -> (!shortSupplyList.contains(den))).toArray(Integer[]::new);
}else if(remaining.get() > 0 && shortSupplyList.size() == 0 && denominations.length > 0){
denom = denominations;
}
return denom;
}

private void recurseSupplyCalculation(Map<Integer, Integer> inventoryMap, List<Integer> shortSupplyList, AtomicInteger remaining, List<Coin> suppliedCoins, Integer[] denom) throws CalculateException {
if (remaining.get() > 0) {
try {
supplyCalculation(denom,inventoryMap, shortSupplyList, remaining, suppliedCoins);
} catch (Exception e) {
log.error("SupplyCalculation Unsuccessful",e);
throw new CalculateException("Insufficient Supply of Coins",e);
}
}
}

private void insufficientInventoryChanges(Map<Integer, Integer> inventoryMap, List<Integer> shortSupplyList, AtomicInteger remaining, List<Coin> suppliedCoins, Integer key) {
remaining.set(remaining.get() - (inventoryMap.get(key) * key));
shortSupplyList.add(key);
addCoin(key, inventoryMap.get(key), suppliedCoins);
zeroValueInventory(inventoryMap, key);
}

private void inventoryAvailableChanges(Map<Integer, Integer> inventoryMap, AtomicInteger remaining, List<Coin> suppliedCoins, Integer key, Coin coin) {
reduceValueInventory(coin.getCount(), inventoryMap, key);
remaining.set(remaining.get() - (coin.getCount() * key));
addCoin(key, coin.getCount(), suppliedCoins);
}

private void zeroValueInventory(Map<Integer, Integer> inventoryMap, Integer key) {
if (inventoryMap.containsKey(key)) {
inventoryMap.put(key, 0);
}
}

private void reduceValueInventory(Integer reducyBy, Map<Integer, Integer> inventoryMap, Integer key) {
if (inventoryMap.containsKey(key)) {
inventoryMap.put(key, (inventoryMap.get(key) - reducyBy));
}
}

private Coin filterByValue(List<Coin> coins, Integer value) throws Exception {
return coins.stream().filter(coin -> coin.getValue() == value).findFirst().get();
}

private boolean isOneofOptimalValue(List<Coin> coins, Integer value){
return coins.stream().anyMatch(coin -> coin.getValue() == value);
}

private void addCoin(Integer value, Integer count, List<Coin> coins) {
Coin coin = new Coin();
coin.setValue(value);
coin.setCount(count);
if(count > 0){
coins.add(coin);
}
}

private Integer inventoryMapValueTotal(Map<Integer, Integer> inventoryMap){
AtomicInteger total = new AtomicInteger(0);
inventoryMap.entrySet().stream().forEach(e ->{
total.set(total.get() + e.getKey() * e.getValue());
});
return total.get();
}
}




@Repository
public class PropertiesReadWriter {

private static final String COIN_INVENTORY_INFORMATION = "Coin-Inventory Information";
private static final String FILE_NAME_COIN_INVENTORY_PROPERTIES = "coin-inventory.properties";
private Map<Integer, Integer> inventoryMap;
public static final Logger log = LoggerFactory.getLogger(PropertiesReadWriter.class);
private String resourceName;

public void readInventoryData() throws Exception {
URL url = this.getClass().getClassLoader().getResource(getResourceName());
Properties prop = new Properties();
InputStream input = new FileInputStream(url.getPath());
try {
prop.load(input);
Map<Integer, Integer> inventoryData = extractInventoryData(prop);
setInventoryMap(inventoryData);
} catch (IOException ex) {
log.error("Unable to Read Sucesssfully", ex);
throw new CalculateException("Unable to Read Sucesssfully", ex);
} finally {
try {
if (input != null) {
input.close();
}
} catch (IOException ex) {
log.error("InputStream Not Closed", ex);
throw new CalculateException("InputStream Not Closed", ex);
}
}
}

public void writeInventoryData(Map<Integer, Integer> inventoryMap) throws Exception {
URL url = this.getClass().getClassLoader().getResource(getResourceName());
Properties prop = new Properties();
Writer writer = new FileWriter(url.getPath());
try {
inventoryMap.entrySet().stream().forEach(coin -> {
prop.setProperty(String.valueOf(coin.getKey()), String.valueOf(coin.getValue()));
});
prop.store(writer, COIN_INVENTORY_INFORMATION);
writer.close();
} catch (IOException ex) {
log.error("Unable to Write Sucesssfully", ex);
throw new CalculateException("Unable to Read Sucesssfully", ex);
} finally {
try {
if (writer != null) {
writer.close();
}
} catch (IOException ex) {
log.error("Writer Not Closed", ex);
throw new CalculateException("Writer Not Closed", ex);
}
}
}

public Integer[] denominations() {
Integer[] denominations = getInventoryMap().keySet().stream().toArray(Integer[]::new);
Arrays.sort(denominations, Collections.reverseOrder());
return denominations;
}

public Map<Integer, Integer> getInventoryMap() {
if (inventoryMap == null) {
inventoryMap = new HashMap<Integer, Integer>();
}
return inventoryMap;
}

public void setInventoryMap(Map<Integer, Integer> inventoryMap) {
this.inventoryMap = inventoryMap;
}

public String getResourceName() {
if (resourceName == null) {
setResourceName(FILE_NAME_COIN_INVENTORY_PROPERTIES);
}
return resourceName;
}

public void setResourceName(String resourceName) {
this.resourceName = resourceName;
}

private Map<Integer, Integer> extractInventoryData(Properties prop) {
return prop.entrySet().stream()
.collect(Collectors.toMap(e -> Integer.valueOf(String.valueOf(e.getKey())), e -> Integer.valueOf(String.valueOf(e.getValue()))));
}
}


5.Create Service Layer for the Calculation.


public interface ChangeService {

public Collection<Coin> getOptimalChangeFor(int pence);

public Collection<Coin> getChangeFor(int pence);

}




@Service
public class ChangeServiceImpl implements ChangeService {

@Autowired
public PropertiesReadWriter propertiesReadWriter;

@Autowired
public Calculate supplyCalculator;

@Autowired
public Calculate optimalCalculator;

public static final Logger log = LoggerFactory.getLogger(ChangeServiceImpl.class);


@Override
public Collection<Coin> getOptimalChangeFor(int pence)  {

try {
return optimalCalculator.calculate(pence,propertiesReadWriter.denominations());
} catch (Exception e) {
log.error("Optimal Calculation not Successful",e);
return Collections.emptyList();
}
}

@Override
public Collection<Coin> getChangeFor(int pence) {
try {
return supplyCalculator.calculate(pence,propertiesReadWriter.denominations());
} catch (Exception e) {
log.error("Supply Calculation not Successful",e);
return Collections.emptyList();
}
}
}


The log and coin inventory properties files are added to the resources.


#coin-inventory
100=11
50=24
20=0
10=99
5=200
2=11
1=23


#log4j.properties
#Root logger option
log4j.rootLogger=INFO,standard


#Direct log messages to standard
log4j.appender.standard=org.apache.log4j.ConsoleAppender
log4j.appender.standard.Target=System.out
log4j.appender.standard.layout=org.apache.log4j.PatternLayout
log4j.appender.standard.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss} %-5p [%t] %c: %m%n


6.Create ChangeController.

Please Refer this link for some more information on annotation RestController.


@RestController
@RequestMapping(value = "/change")
public class ChangeController {
private static final Logger logger = LoggerFactory.getLogger(ChangeController.class);
@Autowired
PropertiesReadWriter propertiesReadWriter;
@Autowired
ChangeService changeServiceImpl;
@RequestMapping(value = "/optimal", method = RequestMethod.GET)
public @ResponseBody List<Coin> handleGetOptimalCalculation(@RequestParam("pence") Integer pence) {
logger.info("ChangeController:handleGetOptimalCalculation Method");
try {
propertiesReadWriter.readInventoryData();
return changeServiceImpl.getOptimalChangeFor(pence).stream().collect(Collectors.toList());
} catch (Exception e) {
logger.info("Error:" + e.getMessage());
return new ArrayList<Coin>();
}
}
@RequestMapping(value = "/supply", method = RequestMethod.GET)
public @ResponseBody List<Coin> handleGetSupplyCalculation(@RequestParam("pence") Integer pence) {
logger.info("ChangeController:handleGetSupplyCalculation Method");
try {
propertiesReadWriter.readInventoryData();
return changeServiceImpl.getChangeFor(pence).stream().collect(Collectors.toList());
} catch (Exception e) {
logger.info("Error:" + e.getMessage());
return new ArrayList<Coin>();
}
}
}


7.Deploy and Run the Rest service.

For Maven building of the project go to the path of the project RestServiceSpringMVC and use the command mvn clean install -Dwtpversion=2.0. After the Build is successful run the application using tomcat7-maven-plugin with command mvn tomcat7:run-war.This which would then run the application with an embedded Apache Tomcat. Now you can call the REST API using any REST Client.Below are the results obtained by calling the REST service using Postman.

Change with Provided denominations

Optimal Change Results

Change with Available denominations

Supply Change Results

8.Download the example.