My Works

Rest Service with Spring Boot.

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

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 Calculation Implementation.
  4. Create Service Layer for the Calculation.
  5. Create ChangeController.
  6. Deploy and Run the Rest service.
  7. Download the example.

1.Create SpringBootRestService project.

Create a SpringBootRestService Project folder. Add a POM file with Project dependencies consisting of JDK 1.8,spring-boot-starter-parent, spring-boot-starter-web,spring-boot-starter-test,jackson-dataformat-xml and spring-boot-starter-tomcat. The pom.xml is available in the downloaded example.The pom file dependencies are listed below.



<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.5.10.RELEASE</version>
<relativePath />
</parent>

<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
</properties>

<dependencies>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
</exclusion>
</exclusions>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>

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

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
<scope>provided</scope>
</dependency>

</dependencies>


2.Create Application Config.

Application Config declares declares one or more @Bean methods, triggers auto-configuration and component scanning.Please Refer this link for some more information on annotation SpringBootApplication.



@ComponentScan(basePackages = { "org.srinivas.siteworks" })
@SpringBootApplication
public class AppConfig extends SpringBootServletInitializer {

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

@Override
protected SpringApplicationBuilder configure(SpringApplicationBuilder application) {
return application.sources(AppConfig.class);
}

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

}


3.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()))));
}
}


4.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


5.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>();
}
}
}


6.Deploy and Run the Rest service.

For Maven building of the project go to the path of the project SpringBootRestService 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.

Spring Boot Deploy Run

Spring Boot Deploy Run

Change with Provided denominations

Optimal Change Results

Change with Available denominations

Supply Change Results

7.Download the example.