Drools integration with Spring vs manual rules creation
Hi!
Often in your work you can come across with issues related to business logic. Let us assume that you have dozens of rules (for the time being in our project we have more than 50 and I used to work on a project where we had hundreds of those rules) that you have to implement, divide into some classes, subclasses, abstractions and of course unit test. This can be difficult and timeconsuming to both write and support. There are many ways of dealing with this problem and I will show you one of them - JBoss Drools.
Drools is a library specifically created for such purposes like implementing rules. As presented in Wikipedia:
"Drools is a rule engine implementaion based on Charles Forgy's Rete Algorithm tailored for the Java language."It contains a rule engine that can process rules wirtten using the Drools language (you can also provide rules in Excel spreadsheets! - perfect for Business side to support and maintain).
In the following example we will take a look at the way one can integrate JBoss Drools with Spring and an example of solving a similar problem without Drools.
Let us assume that we hava a POJO - a product that can represent either a Medical or Electronic product.
Product.java
package pl.grzejszczak.marcin.drools.springintegration.model;What defines the type of a product is the TypeEnum. It also has an outputString - let's assume that it defines a brand of a product (or whatever you want ;) )
import pl.grzejszczak.marcin.drools.springintegration.enums.TypeEnum;
/**
* Created with IntelliJ IDEA.
* User: mgrzejszczak
* Date: 14.01.13
*/
public class Product {
private final TypeEnum typeEnum;
private String productName;
public Product(TypeEnum typeEnum) {
this.typeEnum = typeEnum;
productName = typeEnum.getSomeOutputString();
}
public TypeEnum getTypeEnum() {
return typeEnum;
}
public String getProductName() {
return productName;
}
public void setProductName(String productName) {
this.productName = productName;
}
}
TypeEnum.java
package pl.grzejszczak.marcin.drools.springintegration.enums;
/**
* Created with IntelliJ IDEA.
* User: mgrzejszczak
* Date: 14.01.13
*/
public enum TypeEnum {
MEDICAL("medical", "aaabbbccc"), ELECTRONIC("electronic", "cccbbbaaa");
private final String type;
private final String someOutputString;
private TypeEnum(String type, String someOutputString) {
this.type = type;
this.someOutputString = someOutputString;
}
public String getType() {
return type;
}
public String getSomeOutputString() {
return someOutputString;
}
}
Let's say that the logic behind our rools is such that depending on the type of the enum we want to have some processing done (in our case we will have the same type of processing - converting each 'a' to 'b' in the output string).
NoRulesProductServiceImpl.java
package pl.grzejszczak.marcin.drools.springintegration.service.nondrools;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Component;
import pl.grzejszczak.marcin.drools.springintegration.enums.TypeEnum;
import pl.grzejszczak.marcin.drools.springintegration.model.Product;
import pl.grzejszczak.marcin.drools.springintegration.service.Processor;
import pl.grzejszczak.marcin.drools.springintegration.service.ProductService;
import java.util.List;
import static com.google.common.collect.Lists.newArrayList;
/**
* Created with IntelliJ IDEA.
* User: mgrzejszczak
* Date: 14.01.13
*/
@Component("NoRulesProductServiceImpl")
public class NoRulesProductServiceImpl implements ProductService {
private static final Logger LOGGER = LoggerFactory.getLogger(NoRulesProductServiceImpl.class);
@Autowired
@Qualifier("ProductProcessingService")
private Processor<List<Product>> productProcessingService;
@Override
public void runProductLogic() {
LOGGER.debug("Running product logic without Drools");
Product medicalProduct = new Product(TypeEnum.MEDICAL);
Product electronicProduct = new Product(TypeEnum.ELECTRONIC);
LOGGER.debug("Running rules for products...");
productProcessingService.process(newArrayList(medicalProduct, electronicProduct));
LOGGER.debug("...finished running products.");
}
}
The ProductProcessingService is itterating over the given products, finds a producer for them and processes them.
ProductProcessingService.java
package pl.grzejszczak.marcin.drools.springintegration.service.nondrools;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Component;
import pl.grzejszczak.marcin.drools.springintegration.factory.ProcessingFactory;
import pl.grzejszczak.marcin.drools.springintegration.model.Product;
import pl.grzejszczak.marcin.drools.springintegration.service.Processor;
import java.util.List;
/**
* Created with IntelliJ IDEA.
* User: mgrzejszczak
* Date: 14.01.13
*/
@Component("ProductProcessingService")
public class ProductProcessingService implements Processor<List<Product>> {
@Autowired
@Qualifier("NoRulesProcessingFactory")
private ProcessingFactory<Processor, Product> processingFactory;
@Override
public void process(List<Product> input) {
for(Product product : input){
Processor<Product> processor = processingFactory.createProcessingObject(product);
processor.process(product);
}
}
}
The ProcessingFactory is an interface that basing on the given input (Product) produces an ouput (Processor) that afterwards does further processing. In our case we have a factory that instead of using a bunch of ifs (imagine that we have more than just two types of products) is using a map that matches a type of product with an implementation of a processor. As you can see we change a sequence of ifs into a single get.
NoRulesProcessingFactory.java
package pl.grzejszczak.marcin.drools.springintegration.factory.nondrools;
import com.google.common.collect.ImmutableMap;
import org.springframework.stereotype.Component;
import pl.grzejszczak.marcin.drools.springintegration.enums.TypeEnum;
import pl.grzejszczak.marcin.drools.springintegration.factory.ProcessingFactory;
import pl.grzejszczak.marcin.drools.springintegration.model.Product;
import pl.grzejszczak.marcin.drools.springintegration.service.nondrools.ElectronicProductProcessingService;
import pl.grzejszczak.marcin.drools.springintegration.service.nondrools.MedicalProductProcessingService;
import pl.grzejszczak.marcin.drools.springintegration.service.Processor;
import java.util.Map;
/**
* Created with IntelliJ IDEA.
* User: mgrzejszczak
* Date: 14.01.13
*/
@Component("NoRulesProcessingFactory")
public class NoRulesProcessingFactory implements ProcessingFactory<Processor, Product> {
private static final Map<TypeEnum, Processor> PROCESSOR_MAP = new ImmutableMap.Builder<TypeEnum, Processor>().
put(TypeEnum.MEDICAL, new MedicalProductProcessingService()).
put(TypeEnum.ELECTRONIC, new ElectronicProductProcessingService()).
build();
/**
* By using the map we don't have any ifs
* @param inputObject
* @return
*/
@Override
public Processor createProcessingObject(Product inputObject) {
return PROCESSOR_MAP.get(inputObject.getTypeEnum());
}
}
I will present here only one ProcessingService since the other one is exactly the same (I just wanted to show the concept).
ElectronicProductProcessingService.java
package pl.grzejszczak.marcin.drools.springintegration.service.nondrools;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import pl.grzejszczak.marcin.drools.springintegration.enums.TypeEnum;
import pl.grzejszczak.marcin.drools.springintegration.model.Product;
import pl.grzejszczak.marcin.drools.springintegration.service.Processor;
import pl.grzejszczak.marcin.drools.springintegration.utils.SomeUtil;
import static com.google.common.base.Preconditions.checkArgument;
import static java.lang.String.format;
/**
* Created with IntelliJ IDEA.
* User: mgrzejszczak
* Date: 14.01.13
*/
public class ElectronicProductProcessingService implements Processor<Product> {
private static final Logger LOGGER = LoggerFactory.getLogger(ElectronicProductProcessingService.class);
@Override
public void process(Product input) {
checkArgument(TypeEnum.ELECTRONIC.equals(input.getTypeEnum()), "This processing service works only for electronic devices");
checkArgument(!SomeUtil.replaceAWithB(input.getProductName()).equals(input.getProductName()), "The input has already been processed");
LOGGER.debug("Running processing for Electronic Product");
input.setProductName(SomeUtil.replaceAWithB(input.getProductName()));
LOGGER.debug(format("ELECTRONIC rule applied without Drools, product name is now equal to [%s]", input.getProductName()));
}
}
As you can see there are quite a few things that need to be tested and supported here. Imagine what would happen if we had 100 of types with more sophisticated rules than merely replacing one letter with the other. So how can we do it with Drools? Let's start with taking a look at the pom.xml.
pom.xml
<project xmlns="https://maven.apache.org/POM/4.0.0" xmlns:xsi="https://www.w3.org/2001/XMLSchema-instance"Let's take a look at the applicationContext.xml and the drools-context.xml. As for the first one what we do in fact is just showing where to scan for classes in terms of Spring and where to import the drools context from.
xsi:schemaLocation="https://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>pl.grzejszczak.marcin</groupId>
<artifactId>drools-spring-integration</artifactId>
<version>0.0.1-SNAPSHOT</version>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>1.6</maven.compiler.source>
<maven.compiler.target>1.6</maven.compiler.target>
<spring.version>3.1.1.RELEASE</spring.version>
</properties>
<repositories>
<repository>
<id>spring-release</id>
<url>https://maven.springframework.org/release</url>
</repository>
</repositories>
<dependencies>
<!-- Spring -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>13.0.1</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.drools</groupId>
<artifactId>drools-spring</artifactId>
<version>5.4.0.Final</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>1.6.6</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.10</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>
applicationContext.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="https://www.springframework.org/schema/beans"
xmlns:xsi="https://www.w3.org/2001/XMLSchema-instance"
xmlns:context="https://www.springframework.org/schema/context"
xsi:schemaLocation="https://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context-3.0.xsd
https://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans-3.0.xsd">
<import resource="classpath:drools-context.xml"/>
<!-- Show Spring where to search for the beans (in which packages) -->
<context:component-scan base-package="pl.grzejszczak.marcin.drools.springintegration" />
</beans>
The context for drools. Take a look at the aliases for productsKSession. By providing alias we are joining two potential knowledge sessions into a single one. A single knowledge session is defined for a single knowledge base. For the knowledge base we are providing the list (in our case just a single resource) of drl files (we could have provided an excel spreadsheet).
drools-context.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="https://www.springframework.org/schema/beans"
xmlns:xsi="https://www.w3.org/2001/XMLSchema-instance"
xmlns:drools="https://drools.org/schema/drools-spring"
xsi:schemaLocation="https://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans-3.0.xsd
https://drools.org/schema/drools-spring https://drools.org/schema/drools-spring.xsd">
<!-- KNOWLEDGE BASE FOR A GIVEN TYPE -->
<drools:kbase id="productsKBase">
<drools:resources>
<drools:resource type="DRL"
source="classpath:rules/products.drl"/>
</drools:resources>
</drools:kbase>
<drools:ksession id="productsKSession" name="productsKSession" type="stateless" kbase="productsKBase"/>
<alias name="productsKSession" alias="electronicKSession"/>
<alias name="productsKSession" alias="medicalKSession"/>
</beans>
Let's check the drl file.
We define two rules - "MEDICAL rule" and "ELECTRONIC rule". For each case we are checking:
- whether the input object is of Product type
- whether it has typeEnum equal to either Medical or Electronic
- whether it hasn't already had it's productName changed
Then we are addressing the product by means of a variable $product. We are modifying the product using the modify keyword (which means that all the rules are rechecked - try removing the condition 'productName != replaceAWithB($product.typeEnum.someOutputString' and you will have an endless loop) by setting a new productName. Take a look at all the imports and imports of functions. You can execute a static function (pl.grzejszczak.marcin.drools.springintegration.utils.SomeUtil.replaceAWithB or org.drools.core.util.StringUtils.isEmpty) by importing it in the drl file.
At the end we are logging that a rule has been applied.
products.drl
package pl.grzejszczak.marcin
import org.slf4j.LoggerFactory
import pl.grzejszczak.marcin.drools.springintegration.DroolsSpring
import pl.grzejszczak.marcin.drools.springintegration.model.Product
import pl.grzejszczak.marcin.drools.springintegration.enums.TypeEnum
import function pl.grzejszczak.marcin.drools.springintegration.utils.SomeUtil.replaceAWithB
import function org.drools.core.util.StringUtils.isEmpty
rule "MEDICAL rule"
dialect "mvel"
when
$product : Product( typeEnum == TypeEnum.MEDICAL, productName != replaceAWithB($product.typeEnum.someOutputString) )
then
modify ($product) {productName = replaceAWithB($product.typeEnum.someOutputString)}
LoggerFactory.getLogger(DroolsSpring.class).debug(String.format("MEDICAL rule applied, product name is now equal to [%s]", $product.productName))
end
rule "ELECTRONIC rule"
dialect "mvel"
when
$product : Product( typeEnum == TypeEnum.ELECTRONIC, productName != replaceAWithB($product.typeEnum.someOutputString) )
then
modify ($product) {productName = replaceAWithB($product.typeEnum.someOutputString)}
LoggerFactory.getLogger(DroolsSpring.class).debug(String.format("ELECTRONIC rule applied, product name is now equal to [%s]", $product.productName))
end
We use a factory that is choosing a proper StatelessKnowledgeSession - since we only want to modify an input object. In order to run Drools rules we are running the execute method with a list of input objects.
ProductServiceImpl.java
package pl.grzejszczak.marcin.drools.springintegration.service.drools;Now let's have a look on how the factory is implemented. We are using aliases in the applicationContext.xml
import org.drools.runtime.StatelessKnowledgeSession;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Component;
import pl.grzejszczak.marcin.drools.springintegration.enums.TypeEnum;
import pl.grzejszczak.marcin.drools.springintegration.factory.ProcessingFactory;
import pl.grzejszczak.marcin.drools.springintegration.model.Product;
import pl.grzejszczak.marcin.drools.springintegration.service.ProductService;
import static com.google.common.collect.Lists.newArrayList;
/**
* Created with IntelliJ IDEA.
* User: mgrzejszczak
* Date: 14.01.13
*/
@Component("ProductServiceImpl")
public class ProductServiceImpl implements ProductService {
private static final Logger LOGGER = LoggerFactory.getLogger(ProductServiceImpl.class);
@Autowired
@Qualifier("ProductProcessingFactory")
ProcessingFactory<StatelessKnowledgeSession, Product> processingFactory;
@Override
public void runProductLogic() {
LOGGER.debug("Running product logic");
Product medicalProduct = new Product(TypeEnum.MEDICAL);
Product electronicProduct = new Product(TypeEnum.ELECTRONIC);
StatelessKnowledgeSession statelessKnowledgeSession = processingFactory.createProcessingObject(medicalProduct);
LOGGER.debug("Running rules for products...");
statelessKnowledgeSession.execute(newArrayList(medicalProduct, electronicProduct));
LOGGER.debug("...finished running products.");
}
}
ProductProcessingFactory.java
package pl.grzejszczak.marcin.drools.springintegration.factory.drools;Now how can we test if it works? I have two unit tests that prove it (they are not running in perfect isolation but they functionality of both approaches). Starting with the test for the manual rules creation test.
import org.drools.runtime.StatelessKnowledgeSession;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.stereotype.Component;
import pl.grzejszczak.marcin.drools.springintegration.factory.ProcessingFactory;
import pl.grzejszczak.marcin.drools.springintegration.model.Product;
/**
* Created with IntelliJ IDEA.
* User: mgrzejszczak
* Date: 14.01.13
*/
@Component("ProductProcessingFactory")
public class ProductProcessingFactory implements ProcessingFactory<StatelessKnowledgeSession, Product> {
@Autowired
private ApplicationContext applicationContext;
@Override
public StatelessKnowledgeSession createProcessingObject(Product inputObject) {
return (StatelessKnowledgeSession)applicationContext.getBean(inputObject.getTypeEnum().getType() + "KSession");
}
}
NoRulesProductServiceImplTest.java
package pl.grzejszczak.marcin.drools.springintegration.service;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import pl.grzejszczak.marcin.drools.springintegration.enums.TypeEnum;
import pl.grzejszczak.marcin.drools.springintegration.model.Product;
import pl.grzejszczak.marcin.drools.springintegration.utils.SomeUtil;
import java.util.List;
import static com.google.common.collect.Lists.newArrayList;
import static java.lang.String.format;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotSame;
/**
* Created with IntelliJ IDEA.
* User: mgrzejszczak
* Date: 14.01.13
*/
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = {"classpath:applicationContext.xml"})
public class NoRulesProductServiceImplTest {
@Autowired
@Qualifier("ProductProcessingService")
private Processor<List<Product>> productProcessingService;
/**
* Test is not run in perfect isolation - the purpose is to show the outcome of processing without Drools
*
* @throws Exception
*/
@Test
public void testRunProductLogic() throws Exception {
Product medicalProduct = new Product(TypeEnum.MEDICAL);
Product electronicProduct = new Product(TypeEnum.ELECTRONIC);
String initialMedicalProductName = medicalProduct.getProductName();
String initialElectronicProduct = electronicProduct.getProductName();
System.out.println(format("Initial productName for Medical [%s]", medicalProduct.getProductName()));
System.out.println(format("Initial productName for Electronic [%s]", electronicProduct.getProductName()));
productProcessingService.process(newArrayList(medicalProduct, electronicProduct));
String finalMedicalProduct = medicalProduct.getProductName();
String finalElectronicProduct = electronicProduct.getProductName();
assertNotSame(finalMedicalProduct, initialMedicalProductName);
assertNotSame(finalElectronicProduct, initialElectronicProduct);
assertEquals(SomeUtil.replaceAWithB(initialMedicalProductName), finalMedicalProduct);
assertEquals(SomeUtil.replaceAWithB(initialElectronicProduct), finalElectronicProduct);
System.out.println(format("Final productName for Medical [%s]", medicalProduct.getProductName()));
System.out.println(format("Final productName for Electronic [%s]", electronicProduct.getProductName()));
}
}
And the unit test for the Drools approach.
ProductServiceImplTest.java
package pl.grzejszczak.marcin.drools.springintegration.service;Now let's take a look at the logs - take a look that 'Executing some logic' took place 6 times for Drools since when you modify an object the rules are revalidated and rerun:
import org.drools.runtime.StatelessKnowledgeSession;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import pl.grzejszczak.marcin.drools.springintegration.enums.TypeEnum;
import pl.grzejszczak.marcin.drools.springintegration.factory.ProcessingFactory;
import pl.grzejszczak.marcin.drools.springintegration.model.Product;
import pl.grzejszczak.marcin.drools.springintegration.utils.SomeUtil;
import static com.google.common.collect.Lists.newArrayList;
import static java.lang.String.format;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotSame;
import static org.junit.Assert.assertTrue;
/**
* Created with IntelliJ IDEA.
* User: mgrzejszczak
* Date: 14.01.13
*/
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = {"classpath:applicationContext.xml"})
public class ProductServiceImplTest {
@Autowired
@Qualifier("ProductProcessingFactory")
ProcessingFactory<StatelessKnowledgeSession, Product> processingFactory;
/**
* Test is not run in perfect isolation - the purpose is to show the outcome of processing with Drools
* @throws Exception
*/
@Test
public void testRunProductLogic() throws Exception {
Product medicalProduct = new Product(TypeEnum.MEDICAL);
Product electronicProduct = new Product(TypeEnum.ELECTRONIC);
String initialMedicalProductName = medicalProduct.getProductName();
String initialElectronicProduct = electronicProduct.getProductName();
System.out.println(format("Initial productName for Medical [%s]", medicalProduct.getProductName()));
System.out.println(format("Initial productName for Electronic [%s]", electronicProduct.getProductName()));
StatelessKnowledgeSession statelessKnowledgeSessionForMedical = processingFactory.createProcessingObject(medicalProduct);
StatelessKnowledgeSession statelessKnowledgeSessionForElectronic = processingFactory.createProcessingObject(electronicProduct);
assertTrue(statelessKnowledgeSessionForMedical == statelessKnowledgeSessionForElectronic);
System.out.println("References for stateless sessions are the same, executing rules...");
statelessKnowledgeSessionForMedical.execute(newArrayList(medicalProduct, electronicProduct));
String finalMedicalProduct = medicalProduct.getProductName();
String finalElectronicProduct = electronicProduct.getProductName();
assertNotSame(finalMedicalProduct, initialMedicalProductName);
assertNotSame(finalElectronicProduct, initialElectronicProduct);
assertEquals(SomeUtil.replaceAWithB(initialMedicalProductName), finalMedicalProduct);
assertEquals(SomeUtil.replaceAWithB(initialElectronicProduct), finalElectronicProduct);
System.out.println(format("Final productName for Medical [%s]", medicalProduct.getProductName()));
System.out.println(format("Final productName for Electronic [%s]", electronicProduct.getProductName()));
}
}
org.springframework.context.support.ClassPathXmlApplicationContext:495 Refreshing org.springframework.context.support.ClassPathXmlApplicationContext@743399: startup date [Tue Jan 15 16:32:30 CET 2013]; root of context hierarchy
org.springframework.beans.factory.xml.XmlBeanDefinitionReader:315 Loading XML bean definitions from class path resource [applicationContext.xml]
org.springframework.beans.factory.xml.XmlBeanDefinitionReader:315 Loading XML bean definitions from class path resource [drools-context.xml]
[main] org.springframework.beans.factory.support.DefaultListableBeanFactory:557 Pre-instantiating singletons in org.springframework.beans.factory.support.DefaultListableBeanFactory@3b1d04: defining beans [productsKBase,productsKSession,ProductProcessingFactory,NoRulesProcessingFactory,ProductServiceImpl,NoRulesProductServiceImpl,ProductProcessingService,org.springframework.context.annotation.internalConfigurationAnnotationProcessor,org.springframework.context.annotation.internalAutowiredAnnotationProcessor,org.springframework.context.annotation.internalRequiredAnnotationProcessor,org.springframework.context.annotation.internalCommonAnnotationProcessor,org.springframework.context.annotation.ConfigurationClassPostProcessor$ImportAwareBeanPostProcessor#0]; root of factory hierarchy
pl.grzejszczak.marcin.drools.springintegration.service.drools.ProductServiceImpl:32 Running product logic
pl.grzejszczak.marcin.drools.springintegration.service.drools.ProductServiceImpl:36 Running rules for products...
pl.grzejszczak.marcin.drools.springintegration.utils.SomeUtil:19 Executing some logic
pl.grzejszczak.marcin.drools.springintegration.utils.SomeUtil:19 Executing some logic
pl.grzejszczak.marcin.drools.springintegration.utils.SomeUtil:19 Executing some logic
pl.grzejszczak.marcin.drools.springintegration.utils.SomeUtil:19 Executing some logic
pl.grzejszczak.marcin.drools.springintegration.DroolsSpring:? ELECTRONIC rule applied, product name is now equal to [cccbbbbbb]
pl.grzejszczak.marcin.drools.springintegration.utils.SomeUtil:19 Executing some logic
pl.grzejszczak.marcin.drools.springintegration.utils.SomeUtil:19 Executing some logic
pl.grzejszczak.marcin.drools.springintegration.DroolsSpring:? MEDICAL rule applied, product name is now equal to [bbbbbbccc]
pl.grzejszczak.marcin.drools.springintegration.service.drools.ProductServiceImpl:38 ...finished running products.
pl.grzejszczak.marcin.drools.springintegration.service.nondrools.NoRulesProductServiceImpl:33 Running product logic without Drools
pl.grzejszczak.marcin.drools.springintegration.service.nondrools.NoRulesProductServiceImpl:36 Running rules for products...
pl.grzejszczak.marcin.drools.springintegration.utils.SomeUtil:19 Executing some logic
pl.grzejszczak.marcin.drools.springintegration.service.nondrools.MedicalProductProcessingService:26 Running processing for Medical Product
pl.grzejszczak.marcin.drools.springintegration.utils.SomeUtil:19 Executing some logic
pl.grzejszczak.marcin.drools.springintegration.service.nondrools.MedicalProductProcessingService:28 MEDICAL rule applied without Drools, product name is now equal to [bbbbbbccc]
pl.grzejszczak.marcin.drools.springintegration.utils.SomeUtil:19 Executing some logic
pl.grzejszczak.marcin.drools.springintegration.service.nondrools.ElectronicProductProcessingService:26 Running processing for Electronic Product
pl.grzejszczak.marcin.drools.springintegration.utils.SomeUtil:19 Executing some logic
pl.grzejszczak.marcin.drools.springintegration.service.nondrools.ElectronicProductProcessingService:28 ELECTRONIC rule applied without Drools, product name is now equal to [cccbbbbbb]
pl.grzejszczak.marcin.drools.springintegration.service.nondrools.NoRulesProductServiceImpl:38 ...finished running products.
Viola! That's how you can write some rules with Drools that can save plenty of time and effort as far as business logic is concerned. You can find the sources here at my BitBucket repository.
Very interesting article about XSD
A friend from my project (that will soon start his own blog ;) here is his blog ) has found a very interesting article about XSDs and namespaces.
Tutorial about XSD and namespaces
Tutorial about XSD and namespaces
Hamcrest Matchers, Guava Predicate and Builder design pattern
Hi coding addicts :)
Often, while coding we have to deal with some POJO objects that have dozens of fields in them. Many times we initialize those classes through a constructor having dozens of arguments which is terrible in any possibly imaginable way :) Apart from that the functions that use those constructors are hardly testable. Let's take a closer look at using a Builder to change that situation, together with Hamcrest matchers and Guava Predicates to unit test it.
Let's start off with taking a look at the POJO class.
SomeBigPojo.java
package pl.grzejszczak.marcin.junit.matchers.pojo;Now take a look at the builder class that was used in order to get rid of usage of the humongous constructor. What is more you can set whatever you want to without the need to enter nulls for the undesired fields.
/**
* Created with IntelliJ IDEA.
* User: mgrzejszczak
* Date: 03.01.13
* Time: 21:05
*/
public class SomeBigPojo {
private String stringField0;
private Integer integerField0;
private Boolean booleanField0;
private String stringField1;
private Integer integerField1;
private Boolean booleanField1;
private String stringField2;
private Integer integerField2;
private Boolean booleanField2;
private String stringField3;
private Integer integerField3;
private Boolean booleanField3;
private String stringField4;
private Integer integerField4;
private Boolean booleanField4;
private String stringField5;
private Integer integerField5;
private Boolean booleanField5;
private String stringField6;
private Integer integerField6;
private Boolean booleanField6;
private String stringField7;
private String stringField8;
private String stringField9;
public SomeBigPojo(String stringField0, Integer integerField0, Boolean booleanField0, String stringField1, Integer integerField1, Boolean booleanField1, String stringField2, Integer integerField2, Boolean booleanField2, String stringField3, Integer integerField3, Boolean booleanField3, String stringField4, Integer integerField4, Boolean booleanField4, String stringField5, Integer integerField5, Boolean booleanField5, String stringField6, Integer integerField6, Boolean booleanField6, String stringField7, String stringField8, String stringField9) {
this.stringField0 = stringField0;
this.integerField0 = integerField0;
this.booleanField0 = booleanField0;
this.stringField1 = stringField1;
this.integerField1 = integerField1;
this.booleanField1 = booleanField1;
this.stringField2 = stringField2;
this.integerField2 = integerField2;
this.booleanField2 = booleanField2;
this.stringField3 = stringField3;
this.integerField3 = integerField3;
this.booleanField3 = booleanField3;
this.stringField4 = stringField4;
this.integerField4 = integerField4;
this.booleanField4 = booleanField4;
this.stringField5 = stringField5;
this.integerField5 = integerField5;
this.booleanField5 = booleanField5;
this.stringField6 = stringField6;
this.integerField6 = integerField6;
this.booleanField6 = booleanField6;
this.stringField7 = stringField7;
this.stringField8 = stringField8;
this.stringField9 = stringField9;
}
public String getStringField0() {
return stringField0;
}
public void setStringField0(String stringField0) {
this.stringField0 = stringField0;
}
public Integer getIntegerField0() {
return integerField0;
}
public void setIntegerField0(Integer integerField0) {
this.integerField0 = integerField0;
}
public Boolean getBooleanField0() {
return booleanField0;
}
public void setBooleanField0(Boolean booleanField0) {
this.booleanField0 = booleanField0;
}
public String getStringField1() {
return stringField1;
}
public void setStringField1(String stringField1) {
this.stringField1 = stringField1;
}
public Integer getIntegerField1() {
return integerField1;
}
public void setIntegerField1(Integer integerField1) {
this.integerField1 = integerField1;
}
public Boolean getBooleanField1() {
return booleanField1;
}
public void setBooleanField1(Boolean booleanField1) {
this.booleanField1 = booleanField1;
}
public String getStringField2() {
return stringField2;
}
public void setStringField2(String stringField2) {
this.stringField2 = stringField2;
}
public Integer getIntegerField2() {
return integerField2;
}
public void setIntegerField2(Integer integerField2) {
this.integerField2 = integerField2;
}
public Boolean getBooleanField2() {
return booleanField2;
}
public void setBooleanField2(Boolean booleanField2) {
this.booleanField2 = booleanField2;
}
public String getStringField3() {
return stringField3;
}
public void setStringField3(String stringField3) {
this.stringField3 = stringField3;
}
public Integer getIntegerField3() {
return integerField3;
}
public void setIntegerField3(Integer integerField3) {
this.integerField3 = integerField3;
}
public Boolean getBooleanField3() {
return booleanField3;
}
public void setBooleanField3(Boolean booleanField3) {
this.booleanField3 = booleanField3;
}
public String getStringField4() {
return stringField4;
}
public void setStringField4(String stringField4) {
this.stringField4 = stringField4;
}
public Integer getIntegerField4() {
return integerField4;
}
public void setIntegerField4(Integer integerField4) {
this.integerField4 = integerField4;
}
public Boolean getBooleanField4() {
return booleanField4;
}
public void setBooleanField4(Boolean booleanField4) {
this.booleanField4 = booleanField4;
}
public String getStringField5() {
return stringField5;
}
public void setStringField5(String stringField5) {
this.stringField5 = stringField5;
}
public Integer getIntegerField5() {
return integerField5;
}
public void setIntegerField5(Integer integerField5) {
this.integerField5 = integerField5;
}
public Boolean getBooleanField5() {
return booleanField5;
}
public void setBooleanField5(Boolean booleanField5) {
this.booleanField5 = booleanField5;
}
public String getStringField6() {
return stringField6;
}
public void setStringField6(String stringField6) {
this.stringField6 = stringField6;
}
public Integer getIntegerField6() {
return integerField6;
}
public void setIntegerField6(Integer integerField6) {
this.integerField6 = integerField6;
}
public Boolean getBooleanField6() {
return booleanField6;
}
public void setBooleanField6(Boolean booleanField6) {
this.booleanField6 = booleanField6;
}
public String getStringField7() {
return stringField7;
}
public void setStringField7(String stringField7) {
this.stringField7 = stringField7;
}
public String getStringField8() {
return stringField8;
}
public void setStringField8(String stringField8) {
this.stringField8 = stringField8;
}
public String getStringField9() {
return stringField9;
}
public void setStringField9(String stringField9) {
this.stringField9 = stringField9;
}
@Override
public String toString() {
final StringBuilder sb = new StringBuilder();
sb.append("SomeBigPojo");
sb.append("{stringField0='").append(stringField0).append('\'');
sb.append(", integerField0=").append(integerField0);
sb.append(", booleanField0=").append(booleanField0);
sb.append(", stringField1='").append(stringField1).append('\'');
sb.append(", integerField1=").append(integerField1);
sb.append(", booleanField1=").append(booleanField1);
sb.append(", stringField2='").append(stringField2).append('\'');
sb.append(", integerField2=").append(integerField2);
sb.append(", booleanField2=").append(booleanField2);
sb.append(", stringField3='").append(stringField3).append('\'');
sb.append(", integerField3=").append(integerField3);
sb.append(", booleanField3=").append(booleanField3);
sb.append(", stringField4='").append(stringField4).append('\'');
sb.append(", integerField4=").append(integerField4);
sb.append(", booleanField4=").append(booleanField4);
sb.append(", stringField5='").append(stringField5).append('\'');
sb.append(", integerField5=").append(integerField5);
sb.append(", booleanField5=").append(booleanField5);
sb.append(", stringField6='").append(stringField6).append('\'');
sb.append(", integerField6=").append(integerField6);
sb.append(", booleanField6=").append(booleanField6);
sb.append(", stringField7='").append(stringField7).append('\'');
sb.append(", stringField8='").append(stringField8).append('\'');
sb.append(", stringField9='").append(stringField9).append('\'');
sb.append('}');
return sb.toString();
}
}
SomeBigPojoBuilder.java
package pl.grzejszczak.marcin.junit.matchers.builder;
import pl.grzejszczak.marcin.junit.matchers.pojo.SomeBigPojo;
import static com.google.common.base.Preconditions.checkNotNull;
/**
* Created with IntelliJ IDEA.
* User: mgrzejszczak
* Date: 03.01.13
* Time: 21:08
*/
public class SomeBigPojoBuilder {
/** A field with some default value */
private String stringField0 = "defaultValueForString0";
/** A field with some default value */
private Integer integerField0 = 100;
/** A field with some default value */
private Boolean booleanField0 = true;
private String stringField1;
private Integer integerField1;
private Boolean booleanField1;
private String stringField2;
private Integer integerField2;
private Boolean booleanField2;
private String stringField3;
private Integer integerField3;
private Boolean booleanField3;
private String stringField4;
private Integer integerField4;
private Boolean booleanField4;
private String stringField5;
private Integer integerField5;
private Boolean booleanField5;
private String stringField6;
private Integer integerField6;
private Boolean booleanField6;
private String stringField7;
private String stringField8;
private String stringField9;
public SomeBigPojoBuilder setStringField0(String stringField0) {
this.stringField0 = stringField0;
return this;
}
public SomeBigPojoBuilder setIntegerField0(Integer integerField0) {
this.integerField0 = integerField0;
return this;
}
public SomeBigPojoBuilder setBooleanField0(Boolean booleanField0) {
this.booleanField0 = booleanField0;
return this;
}
public SomeBigPojoBuilder setStringField1(String stringField1) {
this.stringField1 = stringField1;
return this;
}
public SomeBigPojoBuilder setIntegerField1(Integer integerField1) {
this.integerField1 = integerField1;
return this;
}
public SomeBigPojoBuilder setBooleanField1(Boolean booleanField1) {
this.booleanField1 = booleanField1;
return this;
}
public SomeBigPojoBuilder setStringField2(String stringField2) {
this.stringField2 = stringField2;
return this;
}
public SomeBigPojoBuilder setIntegerField2(Integer integerField2) {
this.integerField2 = integerField2;
return this;
}
public SomeBigPojoBuilder setBooleanField2(Boolean booleanField2) {
this.booleanField2 = booleanField2;
return this;
}
public SomeBigPojoBuilder setStringField3(String stringField3) {
this.stringField3 = stringField3;
return this;
}
public SomeBigPojoBuilder setIntegerField3(Integer integerField3) {
this.integerField3 = integerField3;
return this;
}
public SomeBigPojoBuilder setBooleanField3(Boolean booleanField3) {
this.booleanField3 = booleanField3;
return this;
}
public SomeBigPojoBuilder setStringField4(String stringField4) {
this.stringField4 = stringField4;
return this;
}
public SomeBigPojoBuilder setIntegerField4(Integer integerField4) {
this.integerField4 = integerField4;
return this;
}
public SomeBigPojoBuilder setBooleanField4(Boolean booleanField4) {
this.booleanField4 = booleanField4;
return this;
}
public SomeBigPojoBuilder setStringField5(String stringField5) {
this.stringField5 = stringField5;
return this;
}
public SomeBigPojoBuilder setIntegerField5(Integer integerField5) {
this.integerField5 = integerField5;
return this;
}
public SomeBigPojoBuilder setBooleanField5(Boolean booleanField5) {
this.booleanField5 = booleanField5;
return this;
}
public SomeBigPojoBuilder setStringField6(String stringField6) {
this.stringField6 = stringField6;
return this;
}
public SomeBigPojoBuilder setIntegerField6(Integer integerField6) {
this.integerField6 = integerField6;
return this;
}
public SomeBigPojoBuilder setBooleanField6(Boolean booleanField6) {
this.booleanField6 = booleanField6;
return this;
}
public SomeBigPojoBuilder setStringField7(String stringField7) {
this.stringField7 = stringField7;
return this;
}
public SomeBigPojoBuilder setStringField8(String stringField8) {
this.stringField8 = stringField8;
return this;
}
public SomeBigPojoBuilder setStringField9(String stringField9) {
this.stringField9 = stringField9;
return this;
}
/**
* Some function checking the state of our POJO
*/
private void checkState(){
checkNotNull(stringField1, "StringField1 must not be null!");
}
public SomeBigPojo createSomeBigPojoWithBuilder() {
checkState();
return new SomeBigPojo(stringField0, integerField0, booleanField0, stringField1, integerField1, booleanField1, stringField2, integerField2, booleanField2, stringField3, integerField3, booleanField3, stringField4, integerField4, booleanField4, stringField5, integerField5, booleanField5, stringField6, integerField6, booleanField6, stringField7, stringField8, stringField9);
}
}
Take a look at the function checkState that verifies whether the object is of a desired state. In this way we may disallow the creation of an object if some conditions where not fullfiled. For that case we are using the Guava's Predicate - checkNotNull method.
Note that the concept behind the Builder pattern is to delegate creation of an object to a Builder. In fact in the pure Builder design pattern implementation we would have to have a Director, some abstraction over a Builder and the concrete implementation of the Builder as such. In our case we have a simplification of that pattern - I used the refactoring option of IntelliJ. What can be done to make it look even better is to remove the constructor from the POJO and pass the values only by setters (that's what in fact I did in one of my projects ;) )
Now let's take a look at some usage examples:
SomeServiceImpl.java
package pl.grzejszczak.marcin.junit.matchers.service;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import pl.grzejszczak.marcin.junit.matchers.builder.SomeBigPojoBuilder;
import pl.grzejszczak.marcin.junit.matchers.pojo.SomeBigPojo;
import static java.lang.String.format;
/**
* Created with IntelliJ IDEA.
* User: mgrzejszczak
* Date: 03.01.13
* Time: 21:25
*/
public class SomeServiceImpl implements SomeService {
private static final Logger LOGGER = LoggerFactory.getLogger(SomeServiceImpl.class);
/** Could be an abstraction of builders injected by a setter - possible to mock */
private SomeBigPojoBuilder someBigPojoBuilder;
/**
* Hard to unit test
*/
@Override
public void someLogicForAPojoWithoutBuilder() {
LOGGER.debug("SomeLogicForAPojoWithoutBuilder executed");
SomeBigPojo someBigPojo = new SomeBigPojo("string", 1, false, "other string", 123, true, "something else", 321, false, "yet another string", 111, true, "something", 2, false, "More", 3, true, "String", 12, false, "some", "value", "ofString");
// Any chance of knowing what is the value of stringField8 basing on the constructor?
LOGGER.debug(format("StringField8 is equal [%s]%n", someBigPojo.getStringField8()));
// Print the object
LOGGER.debug(someBigPojo.toString());
}
@Override
public void someLogicForAPojoWithBuilder() {
LOGGER.debug("SomeLogicForAPojoWithBuilder executed");
SomeBigPojo someBigPojo = someBigPojoBuilder
.setStringField0("string")
.setIntegerField0(1)
.setBooleanField0(false)
.setStringField1("other string")
.setIntegerField1(123)
.setBooleanField1(true)
.setStringField2("something else")
.setIntegerField2(321)
.setBooleanField2(false)
.setStringField3("yet another string")
.setIntegerField3(111)
.setBooleanField3(false)
.setStringField4("something")
.setIntegerField4(2)
.setBooleanField4(false)
.setStringField5("More")
.setIntegerField5(3)
.setBooleanField5(true)
.setStringField6("String")
.setIntegerField6(12)
.setBooleanField6(false)
.setStringField7("some")
.setStringField8("value")
.setStringField9("ofString")
.createSomeBigPojoWithBuilder();
// Looking at the builder now I guess it's obvious what the value of StringField8
LOGGER.debug(format("StringField8 is equal [%s]%n", someBigPojo.getStringField8()));
// Print the object
LOGGER.debug(someBigPojo.toString());
}
@Override
public void someLogicForAPojoWithBuilderBadArgument() {
LOGGER.debug("someLogicForAPojoWithBuilderBadArgument executed");
SomeBigPojo someBigPojo = someBigPojoBuilder
.setStringField0("string")
.setIntegerField0(1)
.setBooleanField0(true)
.setIntegerField1(123)
.setBooleanField1(true)
.setStringField2("something else")
.setIntegerField2(321)
.setBooleanField2(false)
.setStringField3("yet another string")
.setIntegerField3(111).setBooleanField3(false)
.setStringField4("something")
.setIntegerField4(2)
.setBooleanField4(false)
.setStringField5("More")
.setIntegerField5(3)
.setBooleanField5(true)
.setStringField6("String")
.setIntegerField6(12)
.setBooleanField6(false)
.setStringField7("some")
.setStringField8("value")
.setStringField9("ofString")
.createSomeBigPojoWithBuilder();
// Print the object - will we even see an output
LOGGER.debug(someBigPojo.toString());
}
public void setSomeBigPojoBuilder(SomeBigPojoBuilder someBigPojoBuilder) {
this.someBigPojoBuilder = someBigPojoBuilder;
}
}
Notice how clear it is now to create an object and how easy is to define if a field has been set or not.
Let's move on to Hamcrest matchers that will help us in unit testing of our classes. I will not try to do the complete, 100% code coverage - the idea behind this post is to show how Hamcrest Matchers can become an addition to your unit tests.
Often unit tests are quite unclear and look like this:
SomeBigPojoBuilderNoMatchersAndNoRefactoringTest.java
package pl.grzejszczak.marcin.junit.matchers.builder;
import org.junit.Before;
import org.junit.Test;
import pl.grzejszczak.marcin.junit.matchers.pojo.SomeBigPojo;
import static junit.framework.Assert.assertTrue;
import static org.apache.commons.lang.StringUtils.isNumeric;
import static pl.grzejszczak.marcin.junit.matchers.pojo.SomePojoConstants.*;
/**
* Created with IntelliJ IDEA.
* User: mgrzejszczak
* Date: 03.01.13
* Time: 23:02
*/
public class SomeBigPojoBuilderNoMatchersAndNoRefactoringTest {
private SomeBigPojoBuilder objectUnderTest;
@Before
public void setUp() {
objectUnderTest = new SomeBigPojoBuilder();
}
@Test
public void testCreateSomeBigPojoWithBuilder() throws Exception {
SomeBigPojo someBigPojo = objectUnderTest
.setBooleanField1(true)
.setStringField0("1")
.setStringField1("12")
.setStringField2("123")
.setStringField3("1234")
.setStringField4("12345")
.setStringField5("123456")
.setStringField6("1234567")
.setStringField7("12345678")
.setStringField8("123456789")
.setStringField9("1234567890")
.createSomeBigPojoWithBuilder();
isPojoProperlyBuilt(someBigPojo);
}
@Test(expected = AssertionError.class)
public void testCreateSomeBigPojoWithBuilderWrongFields() throws Exception {
SomeBigPojo someBigPojo = objectUnderTest
.setStringField0("0")
.setStringField1("Too long")
.createSomeBigPojoWithBuilder();
isPojoProperlyBuilt(someBigPojo);
}
private void isPojoProperlyBuilt(SomeBigPojo someBigPojo) {
assertTrue(someBigPojo.getStringField0().length() == STRING_FIELD_0_LENGTH);
assertTrue(isNumeric(someBigPojo.getStringField0()));
assertTrue(someBigPojo.getStringField1().length() == STRING_FIELD_1_LENGTH);
assertTrue(isNumeric(someBigPojo.getStringField0()));
assertTrue(someBigPojo.getStringField2().length() == STRING_FIELD_2_LENGTH);
assertTrue(isNumeric(someBigPojo.getStringField0()));
assertTrue(someBigPojo.getStringField3().length() == STRING_FIELD_3_LENGTH);
assertTrue(isNumeric(someBigPojo.getStringField0()));
assertTrue(someBigPojo.getStringField4().length() == STRING_FIELD_4_LENGTH);
assertTrue(isNumeric(someBigPojo.getStringField0()));
assertTrue(someBigPojo.getStringField5().length() == STRING_FIELD_5_LENGTH);
assertTrue(someBigPojo.getStringField6().length() == STRING_FIELD_6_LENGTH);
assertTrue(someBigPojo.getStringField7().length() == STRING_FIELD_7_LENGTH);
assertTrue(someBigPojo.getStringField8().length() == STRING_FIELD_8_LENGTH);
assertTrue(someBigPojo.getStringField9().length() == STRING_FIELD_9_LENGTH);
}
}
Simple refactoring can make them look nicer...
SomeBigPojoBuilderNoMatchersTest.java
package pl.grzejszczak.marcin.junit.matchers.builder;
import org.junit.Before;
import org.junit.Test;
import pl.grzejszczak.marcin.junit.matchers.pojo.SomeBigPojo;
import static junit.framework.Assert.assertNotNull;
import static junit.framework.Assert.assertTrue;
import static org.apache.commons.lang.StringUtils.isNumeric;
import static pl.grzejszczak.marcin.junit.matchers.pojo.SomePojoConstants.*;
/**
* Created with IntelliJ IDEA.
* User: mgrzejszczak
* Date: 03.01.13
* Time: 23:02
*/
public class SomeBigPojoBuilderNoMatchersTest {
private SomeBigPojoBuilder objectUnderTest;
@Before
public void setUp() {
objectUnderTest = new SomeBigPojoBuilder();
}
@Test
public void testCreateSomeBigPojoWithBuilder() throws Exception {
SomeBigPojo someBigPojo = objectUnderTest
.setBooleanField1(true)
.setStringField0("1")
.setStringField1("12")
.setStringField2("123")
.setStringField3("1234")
.setStringField4("12345")
.setStringField5("123456")
.setStringField6("1234567")
.setStringField7("12345678")
.setStringField8("123456789")
.setStringField9("1234567890")
.createSomeBigPojoWithBuilder();
isPojoProperlyBuilt(someBigPojo);
}
@Test(expected = AssertionError.class)
public void testCreateSomeBigPojoWithBuilderWrongFields() throws Exception {
SomeBigPojo someBigPojo = objectUnderTest
.setStringField0("0")
.setStringField1("too long")
.createSomeBigPojoWithBuilder();
isPojoProperlyBuilt(someBigPojo);
}
private void isPojoProperlyBuilt(SomeBigPojo someBigPojo) {
isOfGivenLength(someBigPojo.getStringField0(), STRING_FIELD_0_LENGTH);
isFieldOfNumericValue(someBigPojo.getStringField0());
isOfGivenLength(someBigPojo.getStringField1(), STRING_FIELD_1_LENGTH);
isFieldOfNumericValue(someBigPojo.getStringField0());
isOfGivenLength(someBigPojo.getStringField2(), STRING_FIELD_2_LENGTH);
isFieldOfNumericValue(someBigPojo.getStringField0());
isOfGivenLength(someBigPojo.getStringField3(), STRING_FIELD_3_LENGTH);
isFieldOfNumericValue(someBigPojo.getStringField0());
isOfGivenLength(someBigPojo.getStringField4(), STRING_FIELD_4_LENGTH);
isFieldOfNumericValue(someBigPojo.getStringField0());
isOfGivenLength(someBigPojo.getStringField5(), STRING_FIELD_5_LENGTH);
isOfGivenLength(someBigPojo.getStringField6(), STRING_FIELD_6_LENGTH);
isOfGivenLength(someBigPojo.getStringField7(), STRING_FIELD_7_LENGTH);
isOfGivenLength(someBigPojo.getStringField8(), STRING_FIELD_8_LENGTH);
isOfGivenLength(someBigPojo.getStringField9(), STRING_FIELD_9_LENGTH);
}
private void isOfGivenLength(String pojo, final Integer expectedLength) {
assertNotNull(pojo);
assertTrue(expectedLength == pojo.length());
}
private void isFieldOfNumericValue(String field) {
assertTrue(isNumeric(field));
}
}
That looks nice, doesn't it? :) And how about using Matchers instead of functions?
SomeBigPojoBuilderTest.kava
package pl.grzejszczak.marcin.junit.matchers.builder;
import org.hamcrest.BaseMatcher;
import org.hamcrest.Description;
import org.hamcrest.Matcher;
import org.junit.Before;
import org.junit.Test;
import pl.grzejszczak.marcin.junit.matchers.pojo.SomeBigPojo;
import static java.lang.String.format;
import static junit.framework.Assert.assertTrue;
import static org.apache.commons.lang.StringUtils.isNumeric;
import static org.junit.Assert.assertThat;
import static pl.grzejszczak.marcin.junit.matchers.pojo.SomePojoConstants.*;
/**
* Created with IntelliJ IDEA.
* User: mgrzejszczak
* Date: 03.01.13
* Time: 23:02
*/
public class SomeBigPojoBuilderTest {
private SomeBigPojoBuilder objectUnderTest;
@Before
public void setUp(){
objectUnderTest = new SomeBigPojoBuilder();
}
@Test
public void testCreateSomeBigPojoWithBuilder() throws Exception {
SomeBigPojo someBigPojo = objectUnderTest
.setBooleanField1(true)
.setStringField0("1")
.setStringField1("12")
.setStringField2("123")
.setStringField3("1234")
.setStringField4("12345")
.setStringField5("123456")
.setStringField6("1234567")
.setStringField7("12345678")
.setStringField8("123456789")
.setStringField9("1234567890")
.createSomeBigPojoWithBuilder();
assertThat(someBigPojo, isPojoProperlyBuilt());
}
@Test(expected = AssertionError.class)
public void testCreateSomeBigPojoWithBuilderWrongFields() throws Exception {
SomeBigPojo someBigPojo = objectUnderTest
.setStringField0("0")
.setStringField1("Too long")
.createSomeBigPojoWithBuilder();
assertThat(someBigPojo, isPojoProperlyBuilt());
}
/**
* Let us assume that there is a specific business case that we have to take into consideration regarding some particular field
*
* @return
*/
private static Matcher isPojoProperlyBuilt() {
return new BaseMatcher() {
@Override
public boolean matches(Object o) {
assertTrue(o instanceof SomeBigPojo);
SomeBigPojo someBigPojo = (SomeBigPojo) o;
assertThat(someBigPojo.getStringField0(), isOfGivenLength(STRING_FIELD_0_LENGTH));
assertThat(someBigPojo.getStringField0(), isFieldOfNumericValue());
assertThat(someBigPojo.getStringField1(), isOfGivenLength(STRING_FIELD_1_LENGTH));
assertThat(someBigPojo.getStringField1(), isFieldOfNumericValue());
assertThat(someBigPojo.getStringField2(), isOfGivenLength(STRING_FIELD_2_LENGTH));
assertThat(someBigPojo.getStringField2(), isFieldOfNumericValue());
assertThat(someBigPojo.getStringField3(), isOfGivenLength(STRING_FIELD_3_LENGTH));
assertThat(someBigPojo.getStringField3(), isFieldOfNumericValue());
assertThat(someBigPojo.getStringField4(), isOfGivenLength(STRING_FIELD_4_LENGTH));
assertThat(someBigPojo.getStringField4(), isFieldOfNumericValue());
assertThat(someBigPojo.getStringField5(), isOfGivenLength(STRING_FIELD_5_LENGTH));
assertThat(someBigPojo.getStringField6(), isOfGivenLength(STRING_FIELD_6_LENGTH));
assertThat(someBigPojo.getStringField7(), isOfGivenLength(STRING_FIELD_7_LENGTH));
assertThat(someBigPojo.getStringField8(), isOfGivenLength(STRING_FIELD_8_LENGTH));
assertThat(someBigPojo.getStringField9(), isOfGivenLength(STRING_FIELD_9_LENGTH));
return true;
}
@Override
public void describeTo(Description description) {
description.appendText("Lengths of fields are limited and the first 4 fields are numeric");
}
};
}
private static Matcher isOfGivenLength(final Integer expectedLength) {
return new BaseMatcher() {
public boolean matches(Object o) {
assertTrue(o instanceof String);
return expectedLength == String.valueOf(o).length();
}
public void describeTo(Description description) {
description.appendText(format("String's length should be equal to [%d]", expectedLength));
}
};
}
private static Matcher isFieldOfNumericValue() {
return new BaseMatcher() {
public boolean matches(Object o) {
assertTrue(o instanceof String);
return isNumeric(String.valueOf(o));
}
public void describeTo(Description description) {
description.appendText("The value of the field should be numeric");
}
};
}
}
The following main method executes the functions of the Service:
package pl.grzejszczak.marcin.junit.matchers;
import pl.grzejszczak.marcin.junit.matchers.builder.SomeBigPojoBuilder;
import pl.grzejszczak.marcin.junit.matchers.service.SomeServiceImpl;
/**
* Created with IntelliJ IDEA.
* User: mgrzejszczak
* Date: 03.01.13
* Time: 22:38
*/
public class MatcherMain {
public static void main(String[] args){
SomeServiceImpl someService = new SomeServiceImpl();
someService.setSomeBigPojoBuilder(new SomeBigPojoBuilder());
someService.someLogicForAPojoWithoutBuilder();
someService.setSomeBigPojoBuilder(new SomeBigPojoBuilder());
someService.someLogicForAPojoWithBuilder();
someService.setSomeBigPojoBuilder(new SomeBigPojoBuilder());
someService.someLogicForAPojoWithBuilderBadArgument();
}
}
And the logs are:
pl.grzejszczak.marcin.junit.matchers.service.SomeServiceImpl:27 SomeLogicForAPojoWithoutBuilder executed
pl.grzejszczak.marcin.junit.matchers.service.SomeServiceImpl:30 StringField8 is equal [value]
pl.grzejszczak.marcin.junit.matchers.service.SomeServiceImpl:32 SomeBigPojo{stringField0='string', integerField0=1, booleanField0=false, stringField1='other string', integerField1=123, booleanField1=true, stringField2='something else', integerField2=321, booleanField2=false, stringField3='yet another string', integerField3=111, booleanField3=true, stringField4='something', integerField4=2, booleanField4=false, stringField5='More', integerField5=3, booleanField5=true, stringField6='String', integerField6=12, booleanField6=false, stringField7='some', stringField8='value', stringField9='ofString'}
pl.grzejszczak.marcin.junit.matchers.service.SomeServiceImpl:37 SomeLogicForAPojoWithBuilder executed
pl.grzejszczak.marcin.junit.matchers.service.SomeServiceImpl:65 StringField8 is equal [value]
pl.grzejszczak.marcin.junit.matchers.service.SomeServiceImpl:67 SomeBigPojo{stringField0='string', integerField0=1, booleanField0=false, stringField1='other string', integerField1=123, booleanField1=true, stringField2='something else', integerField2=321, booleanField2=false, stringField3='yet another string', integerField3=111, booleanField3=false, stringField4='something', integerField4=2, booleanField4=false, stringField5='More', integerField5=3, booleanField5=true, stringField6='String', integerField6=12, booleanField6=false, stringField7='some', stringField8='value', stringField9='ofString'}
pl.grzejszczak.marcin.junit.matchers.service.SomeServiceImpl:72 someLogicForAPojoWithBuilderBadArgument executed
Exception in thread "main" java.lang.NullPointerException: StringField1 must not be null!
at com.google.common.base.Preconditions.checkNotNull(Preconditions.java:208)
at pl.grzejszczak.marcin.junit.matchers.builder.SomeBigPojoBuilder.checkState(SomeBigPojoBuilder.java:166)
at pl.grzejszczak.marcin.junit.matchers.builder.SomeBigPojoBuilder.createSomeBigPojoWithBuilder(SomeBigPojoBuilder.java:170)
at pl.grzejszczak.marcin.junit.matchers.service.SomeServiceImpl.someLogicForAPojoWithBuilderBadArgument(SomeServiceImpl.java:73)
at pl.grzejszczak.marcin.junit.matchers.MatcherMain.main(MatcherMain.java:23)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25)
at java.lang.reflect.Method.invoke(Method.java:597)
at com.intellij.rt.execution.application.AppMain.main(AppMain.java:120)
In my opinion that looks really nice :) And what is yours?
Sources are available here at Too Much Codings code repository.
UPDATE
I've made some code changes and cleaning (not much though cause I didn't have too much time) and the code is available at github - https://github.com/marcingrzejszczak/too-much-coding/tree/master/Unit_Testing_-_Matchers
Google Guava Cache with regular expression patterns
Hi! Merry Christmas everyone :) Quite recently I've seen a nice presentation about Google Guava and we came to the conclusion in our project that it could be really interesting to use the its Cache functionallity. Let us take a look at the regexp Pattern class and its compile function. Quite often in the code we can see that each time a regular expression is being used a programmer is repeatidly calling the aforementioned Pattern.compile() function with the same argument thus compiling the same regular expression over and over again. What could be done however is to cache the result of such compilations - let us take a look at the RegexpUtils utility class:
RegexpUtils.java
package pl.grzejszczak.marcin.guava.cache.utils;As you can see the Guava's LoadingCache with the CacheBuilder is being used to populate a cache with a new compiled pattern if one is not found. Due to caching the compiled pattern if a compilation has already taken place it will not be repeated ever again (in our case since we dno't have any expiry set). Now a simple test
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import java.util.concurrent.ExecutionException;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import static java.lang.String.format;
public final class RegexpUtils {
private RegexpUtils() {
throw new UnsupportedOperationException("RegexpUtils is a utility class - don't instantiate it!");
}
private static final LoadingCache<String, Pattern> COMPILED_PATTERNS =
CacheBuilder.newBuilder().build(new CacheLoader<String, Pattern>() {
@Override
public Pattern load(String regexp) throws Exception {
return Pattern.compile(regexp);
}
});
public static Pattern getPattern(String regexp) {
try {
return COMPILED_PATTERNS.get(regexp);
} catch (ExecutionException e) {
throw new RuntimeException(format("Error when getting a pattern [%s] from cache", regexp), e);
}
}
public static boolean matches(String stringToCheck, String regexp) {
return doGetMatcher(stringToCheck, regexp).matches();
}
public static Matcher getMatcher(String stringToCheck, String regexp) {
return doGetMatcher(stringToCheck, regexp);
}
private static Matcher doGetMatcher(String stringToCheck, String regexp) {
Pattern pattern = getPattern(regexp);
return pattern.matcher(stringToCheck);
}
}
GuavaCache.java
package pl.grzejszczak.marcin.guava.cache;We are running a series of tests and checking the time of their execution. Note that the results of these tests are not precise due to the fact that the application is not being run in isolation so numerous conditions can affect the time of the execution. We are interested in showing some degree of the problem rather than showing the precise execution time. For a given number of iterations (1,10,100,1000,10000,100000,1000000) we are either compiling 10 regular expressions or using a Guava's cache to retrieve the compiled Pattern and then we match them against a string to match. These are the logs:
import com.google.common.base.Stopwatch;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import pl.grzejszczak.marcin.guava.cache.utils.RegexpUtils;
import java.util.regex.Pattern;
import static java.lang.String.format;
public class GuavaCache {
private static final Logger LOGGER = LoggerFactory.getLogger(GuavaCache.class);
public static final String STRING_TO_MATCH = "something";
public static void main(String[] args) {
runTestForManualCompilationAndOneUsingCache(1);
runTestForManualCompilationAndOneUsingCache(10);
runTestForManualCompilationAndOneUsingCache(100);
runTestForManualCompilationAndOneUsingCache(1000);
runTestForManualCompilationAndOneUsingCache(10000);
runTestForManualCompilationAndOneUsingCache(100000);
runTestForManualCompilationAndOneUsingCache(1000000);
}
private static void runTestForManualCompilationAndOneUsingCache(int firstNoOfRepetitions) {
repeatManualCompilation(firstNoOfRepetitions);
repeatCompilationWithCache(firstNoOfRepetitions);
}
private static void repeatManualCompilation(int noOfRepetitions) {
Stopwatch stopwatch = new Stopwatch().start();
compileAndMatchPatternManually(noOfRepetitions);
LOGGER.debug(format("Time needed to compile and check regexp expression [%d] ms, no of iterations [%d]", stopwatch.elapsedMillis(), noOfRepetitions));
}
private static void repeatCompilationWithCache(int noOfRepetitions) {
Stopwatch stopwatch = new Stopwatch().start();
compileAndMatchPatternUsingCache(noOfRepetitions);
LOGGER.debug(format("Time needed to compile and check regexp expression using Cache [%d] ms, no of iterations [%d]", stopwatch.elapsedMillis(), noOfRepetitions));
}
private static void compileAndMatchPatternManually(int limit) {
for (int i = 0; i < limit; i++) {
Pattern.compile("something").matcher(STRING_TO_MATCH).matches();
Pattern.compile("something1").matcher(STRING_TO_MATCH).matches();
Pattern.compile("something2").matcher(STRING_TO_MATCH).matches();
Pattern.compile("something3").matcher(STRING_TO_MATCH).matches();
Pattern.compile("something4").matcher(STRING_TO_MATCH).matches();
Pattern.compile("something5").matcher(STRING_TO_MATCH).matches();
Pattern.compile("something6").matcher(STRING_TO_MATCH).matches();
Pattern.compile("something7").matcher(STRING_TO_MATCH).matches();
Pattern.compile("something8").matcher(STRING_TO_MATCH).matches();
Pattern.compile("something9").matcher(STRING_TO_MATCH).matches();
}
}
private static void compileAndMatchPatternUsingCache(int limit) {
for (int i = 0; i < limit; i++) {
RegexpUtils.matches(STRING_TO_MATCH, "something");
RegexpUtils.matches(STRING_TO_MATCH, "something1");
RegexpUtils.matches(STRING_TO_MATCH, "something2");
RegexpUtils.matches(STRING_TO_MATCH, "something3");
RegexpUtils.matches(STRING_TO_MATCH, "something4");
RegexpUtils.matches(STRING_TO_MATCH, "something5");
RegexpUtils.matches(STRING_TO_MATCH, "something6");
RegexpUtils.matches(STRING_TO_MATCH, "something7");
RegexpUtils.matches(STRING_TO_MATCH, "something8");
RegexpUtils.matches(STRING_TO_MATCH, "something9");
}
}
}
pl.grzejszczak.marcin.guava.cache.GuavaCache:34 Time needed to compile and check regexp expression [1] ms, no of iterations [1]
pl.grzejszczak.marcin.guava.cache.GuavaCache:40 Time needed to compile and check regexp expression using Cache [35] ms, no of iterations [1]
pl.grzejszczak.marcin.guava.cache.GuavaCache:34 Time needed to compile and check regexp expression [1] ms, no of iterations [10]
pl.grzejszczak.marcin.guava.cache.GuavaCache:40 Time needed to compile and check regexp expression using Cache [0] ms, no of iterations [10]
pl.grzejszczak.marcin.guava.cache.GuavaCache:34 Time needed to compile and check regexp expression [8] ms, no of iterations [100]
pl.grzejszczak.marcin.guava.cache.GuavaCache:40 Time needed to compile and check regexp expression using Cache [3] ms, no of iterations [100]
pl.grzejszczak.marcin.guava.cache.GuavaCache:34 Time needed to compile and check regexp expression [10] ms, no of iterations [1000]
pl.grzejszczak.marcin.guava.cache.GuavaCache:40 Time needed to compile and check regexp expression using Cache [10] ms, no of iterations [1000]
pl.grzejszczak.marcin.guava.cache.GuavaCache:34 Time needed to compile and check regexp expression [83] ms, no of iterations [10000]
pl.grzejszczak.marcin.guava.cache.GuavaCache:40 Time needed to compile and check regexp expression using Cache [33] ms, no of iterations [10000]
pl.grzejszczak.marcin.guava.cache.GuavaCache:34 Time needed to compile and check regexp expression [800] ms, no of iterations [100000]
pl.grzejszczak.marcin.guava.cache.GuavaCache:40 Time needed to compile and check regexp expression using Cache [279] ms, no of iterations [100000]
pl.grzejszczak.marcin.guava.cache.GuavaCache:34 Time needed to compile and check regexp expression [7562] ms, no of iterations [1000000]
pl.grzejszczak.marcin.guava.cache.GuavaCache:40 Time needed to compile and check regexp expression using Cache [3067] ms, no of iterations [1000000]
You can find the sources over here under the Guava/Cache directory or go to the url https://bitbucket.org/gregorin1987/too-much-coding/src
Google Guava
Apache Camel with Spring, routing with enrichment service
Sorry for not having posted anything in some time but I had plenty of work. Anyway today I will continue the example with JMS that I've shown you some time ago.
The idea of the previous example was to simplify the work that we had to do in a manual way - we've sed JmsTemplate and Spring listener containers. The routing as such unfortunately was still done by us. In order to facilitate this process we can use Apache Camel.
The following example bases on the one that we've seen in this post Spring JMS, message automatic conversion, JMS template but with slight modifications:
CamelRouter.java
package pl.grzejszczak.marcin.camel;What we can see here is the usage of the Camel's Main class which you can reuse to more easily boot up Camel and keep it running until the JVM terminate.
import org.apache.camel.spring.Main;
public class CamelRouter {
/**
* @param args
* @throws Exception
*/
public static void main(String[] args) throws Exception {
Main main = new Main();
main.setApplicationContextUri("/camel/camelContext.xml");
main.run(args);
}
}
Then we have a new file camelContext.xml in which we have the Camel context in the Spring configuration file.
camelContext.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="https://www.springframework.org/schema/beans"
xmlns:xsi="https://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="https://camel.apache.org/schema/spring https://camel.apache.org/schema/spring/camel-spring.xsd https://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans-3.0.xsd ">
<import resource="classpath:/camel/jmsApplicationContext.xml" />
<camel:camelContext id="camel" xmlns:camel="https://camel.apache.org/schema/spring">
<camel:dataFormats>
<camel:jaxb id="jaxb" prettyPrint="true" contextPath="pl.grzejszczak.marcin.camel.jaxb.generated" />
</camel:dataFormats>
<camel:route>
<camel:from uri="activemq:topic:Initial.Topic" />
<camel:unmarshal ref="jaxb" />
<camel:bean ref="enrichingService" />
<camel:marshal ref="jaxb" />
<camel:to uri="activemq:topic:Routed.Topic" />
</camel:route>
</camel:camelContext>
</beans>
We are defining in this file in order to create a Camel Route - from the activemq topic called Initial.Topic to the one called Routed.Topic. In the meantime we are doing some unmarshalling and marshalling by means of Jaxb.
In the jmsApplicationContext we no longer define the sender to the final topic: Routed.Topic.
jmsApplicationContext.java
<?xml version="1.0" encoding="UTF-8"?>Once we have already initialized our camel context, what we need to do is to send a message to the Initial.Topic. We are doing it by means of our modified ActiveMQRouter class.
<beans xmlns="https://www.springframework.org/schema/beans"
xmlns:xsi="https://www.w3.org/2001/XMLSchema-instance" xmlns:context="https://www.springframework.org/schema/context"
xmlns:jms="https://www.springframework.org/schema/jms" xmlns:oxm="https://www.springframework.org/schema/oxm"
xsi:schemaLocation="https://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context-3.0.xsd https://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans-3.0.xsd https://www.springframework.org/schema/jms https://www.springframework.org/schema/jms/spring-jms-3.0.xsd https://www.springframework.org/schema/oxm https://www.springframework.org/schema/oxm/spring-oxm-3.0.xsd">
<!-- Spring configuration based on annotations -->
<context:annotation-config />
<!-- Show Spring where to search for the beans (in which packages) -->
<context:component-scan base-package="pl.grzejszczak.marcin.camel" />
<!-- Show Spring where to search for the properties files -->
<context:property-placeholder location="classpath:/camel/jms.properties" />
<!-- The ActiveMQ connection factory with specification of the server URL -->
<bean id="activeMQConnectionFactory" class="org.apache.activemq.ActiveMQConnectionFactory">
<property name="brokerURL" value="tcp://localhost:61616" />
</bean>
<!-- Spring's jms connection factory -->
<bean id="cachingConnectionFactory"
class="org.springframework.jms.connection.CachingConnectionFactory">
<property name="targetConnectionFactory" ref="activeMQConnectionFactory" />
<property name="sessionCacheSize" value="10" />
</bean>
<!-- The name of the queue from which we will take the messages -->
<bean id="origin" class="org.apache.activemq.command.ActiveMQTopic">
<constructor-arg value="${jms.origin}" />
</bean>
<!-- The name of the queue to which we will route the messages -->
<bean id="destination" class="org.apache.activemq.command.ActiveMQTopic">
<constructor-arg value="${jms.destination}" />
</bean>
<!-- Configuration of the JmsTemplate together with the connection factory and the message converter -->
<bean id="producerTemplate" class="org.springframework.jms.core.JmsTemplate">
<property name="connectionFactory" ref="cachingConnectionFactory" />
<property name="messageConverter" ref="oxmMessageConverter" />
</bean>
<!-- Custom message sender sending messages to the initial queue -->
<bean id="originPlayerSender" class="pl.grzejszczak.marcin.camel.manual.jms.PlayerDetailsSenderImpl">
<property name="destination" ref="origin" />
</bean>
<!-- Custom message listener - listens to the destination queue -->
<bean id="destinationListenerImpl" class="pl.grzejszczak.marcin.camel.manual.jms.FinalListenerImpl"/>
<!-- Spring's jms message listener container - specified the connection factory, the queue to be listened to and the component that listens to the queue -->
<bean id="jmsDestinationContainer" class="org.springframework.jms.listener.DefaultMessageListenerContainer">
<property name="connectionFactory" ref="cachingConnectionFactory" />
<property name="destination" ref="destination" />
<property name="messageListener" ref="destinationListenerImpl" />
</bean>
<!-- Message converter - automatically marshalls and unmarshalls messages using the provided marshaller / unmarshaller-->
<bean id="oxmMessageConverter" class="org.springframework.jms.support.converter.MarshallingMessageConverter">
<property name="marshaller" ref="marshaller" />
<property name="unmarshaller" ref="marshaller" />
</bean>
<bean id="enrichingService" class="pl.grzejszczak.marcin.camel.service.EnrichingServiceImpl"/>
<!-- Spring's JAXB implementation of marshaller - provided a class the JAXB generated class -->
<oxm:jaxb2-marshaller id="marshaller">
<oxm:class-to-be-bound name="pl.grzejszczak.marcin.camel.jaxb.generated.PlayerDetails" />
</oxm:jaxb2-marshaller>
</beans>
ActiveMQRouter.java
package pl.grzejszczak.marcin.camel.manual;The class is reading the file and sending it to the initial topic. We also initialize a FinalListenerImpl - a class that will listen to the messages coming to the final topic - to prove that everything is working correctly.
import java.io.File;
import java.util.Scanner;
import javax.jms.JMSException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import pl.grzejszczak.marcin.camel.jaxb.PlayerDetailsConverter;
import pl.grzejszczak.marcin.camel.jaxb.generated.PlayerDetails;
import pl.grzejszczak.marcin.camel.manual.jms.FinalListenerImpl;
import pl.grzejszczak.marcin.camel.manual.jms.Sender;
public class ActiveMQRouter {
/**
* @param args
* @throws JMSException
*/
public static void main(String[] args) throws Exception {
ApplicationContext context = new ClassPathXmlApplicationContext("/camel/jmsApplicationContext.xml");
@SuppressWarnings("unchecked")
Sender<PlayerDetails> sender = (Sender<PlayerDetails>) context.getBean("originPlayerSender");
Resource resource = new ClassPathResource("/camel/RobertLewandowski.xml");
Scanner scanner = new Scanner(new File(resource.getURI())).useDelimiter("\\Z");
String contents = scanner.next();
PlayerDetailsConverter converter = context.getBean(PlayerDetailsConverter.class);
FinalListenerImpl listener = (FinalListenerImpl) context.getBean("finalListenerImpl");
sender.sendMessage(converter.unmarshal(contents));
}
}
That's it! Now let's check out the logs. Logs of CamelRouter:
2012-11-22 22:51:39,429 INFO [main] org.apache.camel.main.MainSupport:300 Apache Camel 2.9.2 startingWe can see that the Camel Context has been initialized and then the bean that we have created in the jmsApplicationContext.xml that is listening to the final destination is acknowledging that the message has been enriched properly.
2012-11-22 22:51:40,028 INFO [main] org.springframework.context.support.ClassPathXmlApplicationContext:495 Refreshing org.springframework.context.support.ClassPathXmlApplicationContext@4c5e176f: startup date [Thu Nov 22 22:51:40 CET 2012]; root of context hierarchy
2012-11-22 22:51:40,213 INFO [main] org.springframework.beans.factory.xml.XmlBeanDefinitionReader:315 Loading XML bean definitions from class path resource [camel/camelContext.xml]
2012-11-22 22:51:40,746 INFO [main] org.springframework.beans.factory.xml.XmlBeanDefinitionReader:315 Loading XML bean definitions from class path resource [camel/jmsApplicationContext.xml]
2012-11-22 22:51:41,120 INFO [main] org.springframework.context.annotation.ClassPathBeanDefinitionScanner:210 JSR-330 'javax.inject.Named' annotation found and supported for component scanning
2012-11-22 22:51:43,219 INFO [main] org.springframework.beans.factory.config.PropertyPlaceholderConfigurer:177 Loading properties file from class path resource [camel/jms.properties]
2012-11-22 22:51:43,233 INFO [main] org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor:139 JSR-330 'javax.inject.Inject' annotation found and supported for autowiring
2012-11-22 22:51:43,274 INFO [main] org.springframework.beans.factory.support.DefaultListableBeanFactory:557 Pre-instantiating singletons in org.springframework.beans.factory.support.DefaultListableBeanFactory@19d03a4e: defining beans [org.springframework.context.annotation.internalConfigurationAnnotationProcessor,org.springframework.context.annotation.internalAutowiredAnnotationProcessor,org.springframework.context.annotation.internalRequiredAnnotationProcessor,org.springframework.context.annotation.internalCommonAnnotationProcessor,AgeEnricher,ClubEnricher,PlayerDetailsConverter,finalListenerImpl,playerDetailsSenderImpl,org.springframework.beans.factory.config.PropertyPlaceholderConfigurer#0,activeMQConnectionFactory,cachingConnectionFactory,origin,destination,producerTemplate,originPlayerSender,destinationListenerImpl,jmsDestinationContainer,oxmMessageConverter,enrichingService,marshaller,template,consumerTemplate,camel:beanPostProcessor,camel,org.springframework.context.annotation.ConfigurationClassPostProcessor$ImportAwareBeanPostProcessor#0]; root of factory hierarchy
2012-11-22 22:51:43,424 INFO [main] org.springframework.oxm.jaxb.Jaxb2Marshaller:436 Creating JAXBContext with classes to be bound [class pl.grzejszczak.marcin.camel.jaxb.generated.PlayerDetails]
2012-11-22 22:51:44,521 INFO [main] org.springframework.context.support.DefaultLifecycleProcessor:334 Starting beans in phase 2147483647
2012-11-22 22:51:45,061 INFO [main] org.springframework.jms.connection.CachingConnectionFactory:291 Established shared JMS Connection: ActiveMQConnection {id=ID:marcin-SR700-47684-1353621104666-1:1,clientId=null,started=false}
2012-11-22 22:51:45,608 INFO [main] org.apache.camel.spring.SpringCamelContext:1374 Apache Camel 2.9.2 (CamelContext: camel) is starting
2012-11-22 22:51:45,611 INFO [main] org.apache.camel.management.ManagementStrategyFactory:38 JMX enabled. Using ManagedManagementStrategy.
2012-11-22 22:51:45,850 INFO [main] org.apache.camel.management.DefaultManagementLifecycleStrategy:790 StatisticsLevel at All so enabling load performance statistics
2012-11-22 22:51:45,961 INFO [main] org.apache.camel.impl.converter.AnnotationTypeConverterLoader:119 Found 3 packages with 15 @Converter classes to load
2012-11-22 22:51:45,995 INFO [main] org.apache.camel.impl.converter.DefaultTypeConverter:405 Loaded 170 core type converters (total 170 type converters)
2012-11-22 22:51:46,002 INFO [main] org.apache.camel.impl.converter.AnnotationTypeConverterLoader:109 Loaded 2 @Converter classes
2012-11-22 22:51:46,023 INFO [main] org.apache.camel.impl.converter.AnnotationTypeConverterLoader:119 Found 1 packages with 1 @Converter classes to load
2012-11-22 22:51:46,024 WARN [main] org.apache.camel.impl.converter.DefaultTypeConverter:257 Overriding type converter from: StaticMethodTypeConverter: public static org.apache.activemq.command.ActiveMQDestination org.apache.camel.component.activemq.ActiveMQConverter.toDestination(java.lang.String) to: StaticMethodTypeConverter: public static org.apache.activemq.command.ActiveMQDestination org.apache.activemq.camel.converter.ActiveMQConverter.toDestination(java.lang.String)
2012-11-22 22:51:46,043 INFO [main] org.apache.camel.impl.converter.DefaultTypeConverter:431 Loaded additional 3 type converters (total 173 type converters) in 0.041 seconds
2012-11-22 22:51:46,360 INFO [main] org.apache.camel.converter.jaxb.JaxbDataFormat:277 Creating JAXBContext with contextPath: pl.grzejszczak.marcin.camel.jaxb.generated and ApplicationContextClassLoader: sun.misc.Launcher$AppClassLoader@35a16869
2012-11-22 22:51:46,500 INFO [main] org.apache.camel.spring.SpringCamelContext:1980 Route: route1 started and consuming from: Endpoint[activemq://topic:Initial.Topic]
2012-11-22 22:51:46,509 INFO [main] org.apache.camel.spring.SpringCamelContext:1409 Total 1 routes, of which 1 is started.
2012-11-22 22:51:46,510 INFO [main] org.apache.camel.spring.SpringCamelContext:1410 Apache Camel 2.9.2 (CamelContext: camel) started in 0.901 seconds
2012-11-22 22:51:46,519 INFO [main] org.springframework.context.support.DefaultLifecycleProcessor:334 Starting beans in phase 2147483647
2012-11-22 22:52:08,375 DEBUG [Camel (camel) thread #1 - JmsConsumer[Initial.Topic]] pl.grzejszczak.marcin.camel.service.EnrichingServiceImpl:21 Enriching player details
2012-11-22 22:52:08,377 DEBUG [Camel (camel) thread #1 - JmsConsumer[Initial.Topic]] pl.grzejszczak.marcin.camel.enricher.AgeEnricher:17 Enriching player [Lewandowski] with age data
2012-11-22 22:52:10,379 DEBUG [Camel (camel) thread #1 - JmsConsumer[Initial.Topic]] pl.grzejszczak.marcin.camel.enricher.ClubEnricher:16 Enriching player [Lewandowski] with club data
2012-11-22 22:52:12,462 DEBUG [jmsDestinationContainer-1] pl.grzejszczak.marcin.camel.manual.jms.FinalListenerImpl:35 Message already enriched! Shutting down the system
What about the ActiveMQRouter.java logs?
2012-11-22 22:52:06,077 INFO [main] org.springframework.context.support.ClassPathXmlApplicationContext:495 Refreshing org.springframework.context.support.ClassPathXmlApplicationContext@43462851: startup date [Thu Nov 22 22:52:06 CET 2012]; root of context hierarchyFirst we see that our spring Context has been initialized and then we see that a message has been sent to the Initial.Topic. At the end of the processing we can see that the listener is confirming that the message has been properly enriched - so all the Camel work has been done in a proper way.
2012-11-22 22:52:06,153 INFO [main] org.springframework.beans.factory.xml.XmlBeanDefinitionReader:315 Loading XML bean definitions from class path resource [camel/jmsApplicationContext.xml]
2012-11-22 22:52:06,417 INFO [main] org.springframework.context.annotation.ClassPathBeanDefinitionScanner:210 JSR-330 'javax.inject.Named' annotation found and supported for component scanning
2012-11-22 22:52:06,721 INFO [main] org.springframework.beans.factory.config.PropertyPlaceholderConfigurer:177 Loading properties file from class path resource [camel/jms.properties]
2012-11-22 22:52:06,733 INFO [main] org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor:139 JSR-330 'javax.inject.Inject' annotation found and supported for autowiring
2012-11-22 22:52:06,758 INFO [main] org.springframework.beans.factory.support.DefaultListableBeanFactory:557 Pre-instantiating singletons in org.springframework.beans.factory.support.DefaultListableBeanFactory@362f0d54: defining beans [org.springframework.context.annotation.internalConfigurationAnnotationProcessor,org.springframework.context.annotation.internalAutowiredAnnotationProcessor,org.springframework.context.annotation.internalRequiredAnnotationProcessor,org.springframework.context.annotation.internalCommonAnnotationProcessor,AgeEnricher,ClubEnricher,PlayerDetailsConverter,finalListenerImpl,playerDetailsSenderImpl,org.springframework.beans.factory.config.PropertyPlaceholderConfigurer#0,activeMQConnectionFactory,cachingConnectionFactory,origin,destination,producerTemplate,originPlayerSender,destinationListenerImpl,jmsDestinationContainer,oxmMessageConverter,enrichingService,marshaller,org.springframework.context.annotation.ConfigurationClassPostProcessor$ImportAwareBeanPostProcessor#0]; root of factory hierarchy
2012-11-22 22:52:07,224 INFO [main] org.springframework.oxm.jaxb.Jaxb2Marshaller:436 Creating JAXBContext with classes to be bound [class pl.grzejszczak.marcin.camel.jaxb.generated.PlayerDetails]
2012-11-22 22:52:07,628 INFO [main] org.springframework.context.support.DefaultLifecycleProcessor:334 Starting beans in phase 2147483647
2012-11-22 22:52:07,883 INFO [main] org.springframework.jms.connection.CachingConnectionFactory:291 Established shared JMS Connection: ActiveMQConnection {id=ID:marcin-SR700-53586-1353621127755-1:1,clientId=null,started=false}
2012-11-22 22:52:08,093 DEBUG [main] pl.grzejszczak.marcin.camel.manual.jms.PlayerDetailsSenderImpl:26 Sending [pl.grzejszczak.marcin.camel.jaxb.generated.PlayerDetails@3ea86d12] to topic [topic://Initial.Topic]
2012-11-22 22:52:12,463 DEBUG [jmsDestinationContainer-1] pl.grzejszczak.marcin.camel.manual.jms.FinalListenerImpl:35 Message already enriched! Shutting down the system
This example is showing how easy and simple it can be to create a routing / enriching service by means of Spring and Camel (integrated with Spring).
The sources are available at Too Much Coding's repository at bitbucket.
Sources to blog posts
I am happy to announce that I managed to find some time in order to create a repository for the code that I've been presenting here (at least for the most of it). You can find the Too Much Coding sources by clicking here or type in the address https://bitbucket.org/gregorin1987/too-much-coding.
Enjoy :)
Enjoy :)
Spring JMS, message automatic conversion, JMS template
Hi!
In one of my projects I was supposed to create a message router that like all routers was supposed to take the JMS messages from one topic and put it into another one. The message itself was a JMS text message that in fact contained an XML message. What is more after having received it I was supposed to enrich the message with some additional data.
We were not allowed to use neither Spring nor JAXB nor any other useful library so I decided to check how easy it would be to do it using them. Initially I wanted to use only Spring and JAXB but in the next post I will try to repeat the same scenario by using Apache Camel (that's why you will find the word "camel" in the package name). The JMS communication was present thanks to the ActiveMQ messaging server.
Anyway coming back to the code.
I used maven to resolve dependencies and these are the dependencies that were mandatory i n terms of JMS and JAXB and message conversion:
pom.xml
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jms</artifactId>
<version>3.1.1.RELEASE</version>
</dependency>
<dependency>
<groupId>com.sun.xml.bind</groupId>
<artifactId>jaxb-impl</artifactId>
<version>2.2.6</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-oxm</artifactId>
<version>3.1.1.RELEASE</version>
</dependency>
This is how I divided the project (the camel part of the package will make more sense in the next article).
In order to have my message converted to objects via JAXB I needed a schema:
Player.xsd
<?xml version="1.0" encoding="UTF-8"?>
<xsd:schema xmlns:xsd="https://www.w3.org/2001/XMLSchema">
<xsd:element name="PlayerDetails">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="Name" type="xsd:string" />
<xsd:element name="Surname" type="xsd:string" />
<xsd:element name="Position" type="PositionType" />
<xsd:element name="Age" type="xsd:int" />
<xsd:element name="TeamName" type="xsd:string" />
</xsd:sequence>
</xsd:complexType>
</xsd:element>
<xsd:simpleType name="PositionType">
<xsd:restriction base="xsd:string">
<xsd:enumeration value="GK" />
<xsd:enumeration value="DEF" />
<xsd:enumeration value="MID" />
<xsd:enumeration value="ATT" />
</xsd:restriction>
</xsd:simpleType>
</xsd:schema>
I had to download JAXB binaries and executed the following command to have my objects created:
./xjc.sh -p pl.grzejszczak.marcin.camel.jaxb.generated ~/PATH/TO/THE/SCHEMA/FILE/Player.xsd
Note: The same you can achieve by using maven. This approach is not in the blog's repository but believe me - it does work :D
Add dependency to pom
<dependency>
<groupId>javax.xml.bind</groupId>
<artifactId>jaxb-api</artifactId>
<version>2.1</version>
</dependency>
Use the plugin (mind that the schema file needs to be specified or by default is searched for at src/main/xsd/ folder)
<build>
<pluginManagement>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>2.5.1</version>
</plugin>
</plugins>
</pluginManagement>
<plugins>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>jaxb2-maven-plugin</artifactId>
<version>1.5</version>
<executions>
<execution>
<id>xjc</id>
<goals>
<goal>xjc</goal>
</goals>
</execution>
</executions>
<configuration>
<packageName>pl.grzejszczak.marcin.camel.jaxb.generated</packageName>
</configuration>
</plugin>
</plugins>
</build>
An example of the outcome of this command or maven plugin is the following:
PlayerDetails.java
//The @XmlRootElement(name = "PlayerDetails") means that this class will output a Root node in the XML file. The @XmlAccessorType(XmlAccessType.FIELD) as the JavaDoc says means that "Every non static, non transient field in a JAXB-bound class will be automatically bound to XML, unless annotated by XmlTransient." In other words, if you have a field annotated by the XmlTransient annotation it won't get serialized. Then we have the @XmlType(name = "", propOrder = { "name", "surname", "position", "age", "teamName" })which as JavaDoc sates "Maps a class or an enum type to a XML Schema type" . In other words our class is mapped to the PlayerDetails element in the schema. Finally we have the @XmlElement(name = "Name", required = true) annotation which is a mapping of the XML node (element) to a field in the class.
// This file was generated by the JavaTM Architecture for XML Binding(JAXB) Reference Implementation, v2.2.6
// See https://java.sun.com/xml/jaxb
// Any modifications to this file will be lost upon recompilation of the source schema.
// Generated on: 2012.11.05 at 09:23:22 PM CET
//
package pl.grzejszczak.marcin.camel.jaxb.generated;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlRootElement;
import javax.xml.bind.annotation.XmlType;
/**
* Java class for anonymous complex type.
*
*
The following schema fragment specifies the expected content contained within this class.
*
** <complexType>* * */ @XmlAccessorType(XmlAccessType.FIELD) @XmlType(name = "", propOrder = { "name", "surname", "position", "age", "teamName" }) @XmlRootElement(name = "PlayerDetails") public class PlayerDetails { @XmlElement(name = "Name", required = true) protected String name; @XmlElement(name = "Surname", required = true) protected String surname; @XmlElement(name = "Position", required = true) protected PositionType position; @XmlElement(name = "Age") protected int age; @XmlElement(name = "TeamName", required = true) protected String teamName; /** * Gets the value of the name property. * * @return * possible object is * {@link String } * */ public String getName() { return name; } /** * Sets the value of the name property. * * @param value * allowed object is * {@link String } * */ public void setName(String value) { this.name = value; } /** * Gets the value of the surname property. * * @return * possible object is * {@link String } * */ public String getSurname() { return surname; } /** * Sets the value of the surname property. * * @param value * allowed object is * {@link String } * */ public void setSurname(String value) { this.surname = value; } /** * Gets the value of the position property. * * @return * possible object is * {@link PositionType } * */ public PositionType getPosition() { return position; } /** * Sets the value of the position property. * * @param value * allowed object is * {@link PositionType } * */ public void setPosition(PositionType value) { this.position = value; } /** * Gets the value of the age property. * */ public int getAge() { return age; } /** * Sets the value of the age property. * */ public void setAge(int value) { this.age = value; } /** * Gets the value of the teamName property. * * @return * possible object is * {@link String } * */ public String getTeamName() { return teamName; } /** * Sets the value of the teamName property. * * @param value * allowed object is * {@link String } * */ public void setTeamName(String value) { this.teamName = value; } }
* <complexContent>
* <restriction base="{https://www.w3.org/2001/XMLSchema}anyType">
* <sequence>
* <element name="Name" type="{https://www.w3.org/2001/XMLSchema}string"/>
* <element name="Surname" type="{https://www.w3.org/2001/XMLSchema}string"/>
* <element name="Position" type="{}PositionType"/>
* <element name="Age" type="{https://www.w3.org/2001/XMLSchema}int"/>
* <element name="TeamName" type="{https://www.w3.org/2001/XMLSchema}string"/>
* </sequence>
* </restriction>
* </complexContent>
* </complexType>
*
This is my message to be sent, received, enriched and routed:
RobertLewandowski.xml
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<PlayerDetails>
<Name>Robert</Name>
<Surname>Lewandowski</Surname>
<Position>ATT</Position>
</PlayerDetails>
Now off to my JMS configuration - I have configured the Queues of origin and destination
jms.properties
jms.origin=Initial.Queue
jms.destination=Routed.Queue
This is my Spring configuration (I added comments inside the config that explain the origin of those components):
jmsApplicationContext.xml
<?xml version="1.0" encoding="UTF-8"?>Now let's take a look at the Java code - let's start with the class that has the main function
<beans xmlns="https://www.springframework.org/schema/beans"
xmlns:xsi="https://www.w3.org/2001/XMLSchema-instance" xmlns:context="https://www.springframework.org/schema/context"
xmlns:jms="https://www.springframework.org/schema/jms" xmlns:oxm="https://www.springframework.org/schema/oxm"
xsi:schemaLocation="https://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context-3.0.xsd https://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans-3.0.xsd https://www.springframework.org/schema/jms https://www.springframework.org/schema/jms/spring-jms-3.0.xsd https://www.springframework.org/schema/oxm https://www.springframework.org/schema/oxm/spring-oxm-3.0.xsd">
<!-- Spring configuration based on annotations -->
<context:annotation-config />
<!-- Show Spring where to search for the beans (in which packages) -->
<context:component-scan base-package="pl.grzejszczak.marcin.camel" />
<!-- Show Spring where to search for the properties files -->
<context:property-placeholder location="classpath:/camel/jms.properties" />
<!-- The ActiveMQ connection factory with specification of the server URL -->
<bean id="activeMQConnectionFactory" class="org.apache.activemq.ActiveMQConnectionFactory">
<property name="brokerURL" value="tcp://localhost:61616" />
</bean>
<!-- Spring's jms connection factory -->
<bean id="cachingConnectionFactory"
class="org.springframework.jms.connection.CachingConnectionFactory">
<property name="targetConnectionFactory" ref="activeMQConnectionFactory" />
<property name="sessionCacheSize" value="10" />
</bean>
<!-- The name of the queue from which we will take the messages -->
<bean id="origin" class="org.apache.activemq.command.ActiveMQQueue">
<constructor-arg value="${jms.origin}" />
</bean>
<!-- The name of the queue to which we will route the messages -->
<bean id="destination" class="org.apache.activemq.command.ActiveMQQueue">
<constructor-arg value="${jms.destination}" />
</bean>
<!-- Configuration of the JmsTemplate together with the connection factory and the message converter -->
<bean id="producerTemplate" class="org.springframework.jms.core.JmsTemplate">
<property name="connectionFactory" ref="cachingConnectionFactory" />
<property name="messageConverter" ref="oxmMessageConverter" />
</bean>
<!-- Custom message sender sending messages to the initial queue -->
<bean id="originPlayerSender" class="pl.grzejszczak.marcin.camel.manual.jms.PlayerDetailsSenderImpl">
<property name="destination" ref="origin" />
</bean>
<!-- Custom message sender sending messages to the destination queue -->
<bean id="destinationPlayerSender" class="pl.grzejszczak.marcin.camel.manual.jms.PlayerDetailsSenderImpl">
<property name="destination" ref="destination" />
</bean>
<!-- Custom message listener - listens to the initial queue -->
<bean id="originListenerImpl" class="pl.grzejszczak.marcin.camel.manual.jms.ListenerImpl"/>
<!-- Custom message listener - listens to the destination queue -->
<bean id="destinationListenerImpl" class="pl.grzejszczak.marcin.camel.manual.jms.FinalListenerImpl"/>
<!-- Spring's jms message listener container - specified the connection factory, the queue to be listened to and the component that listens to the queue -->
<bean id="jmsOriginContainer" class="org.springframework.jms.listener.DefaultMessageListenerContainer">
<property name="connectionFactory" ref="cachingConnectionFactory" />
<property name="destination" ref="origin" />
<property name="messageListener" ref="originListenerImpl" />
</bean>
<!-- Spring's jms message listener container - specified the connection factory, the queue to be listened to and the component that listens to the queue -->
<bean id="jmsDestinationContainer" class="org.springframework.jms.listener.DefaultMessageListenerContainer">
<property name="connectionFactory" ref="cachingConnectionFactory" />
<property name="destination" ref="destination" />
<property name="messageListener" ref="destinationListenerImpl" />
</bean>
<!-- Message converter - automatically marshalls and unmarshalls messages using the provided marshaller / unmarshaller-->
<bean id="oxmMessageConverter" class="org.springframework.jms.support.converter.MarshallingMessageConverter">
<property name="marshaller" ref="marshaller" />
<property name="unmarshaller" ref="marshaller" />
</bean>
<!-- Spring's JAXB implementation of marshaller - provided a class the JAXB generated class -->
<oxm:jaxb2-marshaller id="marshaller">
<oxm:class-to-be-bound name="pl.grzejszczak.marcin.camel.jaxb.generated.PlayerDetails" />
</oxm:jaxb2-marshaller>
</beans>
ActiveMQRouter.java
package pl.grzejszczak.marcin.camel.manual;What we can see here is that we initialize the Spring context from the classpath and retrieve the bean named originPlayerSender. This component is used for sending a message to the initial queue. In order to have a message to send we are retrieving a file RobertLewandowski.xml from the classpath and read it to a String variable through the Scanner class. Next we use our custom PlayerDetailsConverter class to unmarshall the String contents into a PlayerDetails object, which in effect is sent by the originPlayerSender to the origin queue.
import java.io.File;
import java.util.Scanner;
import javax.jms.JMSException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import pl.grzejszczak.marcin.camel.jaxb.PlayerDetailsConverter;
import pl.grzejszczak.marcin.camel.jaxb.generated.PlayerDetails;
import pl.grzejszczak.marcin.camel.manual.jms.Sender;
public class ActiveMQRouter {
/**
* @param args
* @throws JMSException
*/
public static void main(String[] args) throws Exception {
ApplicationContext context = new ClassPathXmlApplicationContext("/camel/jmsApplicationContext.xml");
@SuppressWarnings("unchecked")
Sender<PlayerDetails> sender = (Sender<PlayerDetails>) context.getBean("originPlayerSender");
Resource resource = new ClassPathResource("/camel/RobertLewandowski.xml");
Scanner scanner = new Scanner(new File(resource.getURI())).useDelimiter("\\Z");
String contents = scanner.next();
PlayerDetailsConverter converter = context.getBean(PlayerDetailsConverter.class);
sender.sendMessage(converter.unmarshal(contents));
}
}
Now let's take a look at the sender logic:
PlayerDetailsSenderImpl.java
package pl.grzejszczak.marcin.camel.manual.jms;This class is implementing my Sender interface that provides the sendMessage function. We are using the JmsTemplate object to convert and send the message to the given destination that is injected via Spring.
import javax.jms.Destination;
import javax.jms.JMSException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jms.core.JmsTemplate;
import org.springframework.stereotype.Component;
import pl.grzejszczak.marcin.camel.jaxb.generated.PlayerDetails;
@Component
public class PlayerDetailsSenderImpl implements Sender<PlayerDetails> {
private static final Logger LOGGER = LoggerFactory.getLogger(PlayerDetailsSenderImpl.class);
private Destination destination;
@Autowired
private JmsTemplate jmsTemplate;
@Override
public void sendMessage(final PlayerDetails object) throws JMSException {
LOGGER.debug("Sending [{}] to topic [{}]", new Object[] { object, destination });
jmsTemplate.convertAndSend(destination, object);
}
public Destination getDestination() {
return destination;
}
public void setDestination(Destination destination) {
this.destination = destination;
}
}
Ok, now that we've sent the message someone has to retrieve it:
ListenerImpl.java
package pl.grzejszczak.marcin.camel.manual.jms;This class has the list of all the classes implementing the Enrichable interface thanks to which it will provide the enrichment of the message without the necessity of knowing the amount of enrichers in the system. There is also the PlayerDetailsConverter class that helps with marshalling and unmarshalling PlayerDetails. Once the message is enriched it is sent to the destination queue through the bean that implements the Sender interface and has the id of destinationPlayerSender. It is important to remember that what we receive from the queue is a BytesMessage thus that's why we are doing the initial check.
import java.util.List;
import javax.jms.BytesMessage;
import javax.jms.Message;
import javax.jms.MessageListener;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.jms.support.converter.MessageConverter;
import org.springframework.stereotype.Component;
import pl.grzejszczak.marcin.camel.enricher.Enrichable;
import pl.grzejszczak.marcin.camel.jaxb.Convertable;
import pl.grzejszczak.marcin.camel.jaxb.generated.PlayerDetails;
@Component
public class ListenerImpl implements MessageListener {
private static final Logger LOG = LoggerFactory.getLogger(ListenerImpl.class);
@Autowired
private Convertable<PlayerDetails> playerDetailsConverter;
@Autowired
private List<Enrichable<PlayerDetails>> listOfEnrichers;
@Autowired
private MessageConverter messageConverter;
@Autowired
@Qualifier("destinationPlayerSender")
private Sender<PlayerDetails> sender;
@Override
public void onMessage(Message message) {
if (!(message instanceof BytesMessage)) {
LOG.error("Wrong msg!");
return;
}
PlayerDetails playerDetails = null;
try {
playerDetails = (PlayerDetails) messageConverter.fromMessage(message);
LOG.debug("Enriching the input message");
for (Enrichable<PlayerDetails> enrichable : listOfEnrichers) {
enrichable.enrich(playerDetails);
}
LOG.debug("Enriched text message: [{}]", new Object[] { playerDetailsConverter.marshal(playerDetails) });
sender.sendMessage(playerDetails);
} catch (Exception e) {
LOG.error("Exception occured", e);
}
}
}
Let's take a look at one of the enrichers (the other one is a setting another field in the PlayerDetails object)
ClubEnricher.java
package pl.grzejszczak.marcin.camel.enricher;As you can see the class is just simulating some access to the DB or any other service and afterwards is setting the team name in the input PlayerDetails object.
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import pl.grzejszczak.marcin.camel.jaxb.generated.PlayerDetails;
@Component("ClubEnricher")
public class ClubEnricher implements Enrichable<PlayerDetails> {
private static final Logger LOGGER = LoggerFactory.getLogger(ClubEnricher.class);
@Override
public void enrich(PlayerDetails inputObject) {
LOGGER.debug("Enriching player [{}] with club data", new Object[] { inputObject.getSurname() });
// Simulating accessing DB or some other service
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
LOGGER.error("Exception while sleeping occured", e);
}
inputObject.setTeamName("Borussia Dortmund");
}
}
Let's now take a look a the conversion mechanism:
PlayerDetailsConverter.java
package pl.grzejszczak.marcin.camel.jaxb;In the constructor we are setting some JAXB components - the JAXBContext, JAXB Marshaller and JAXB Unmarshaller that have the necessary marshal and unmarshal methods.
import java.io.ByteArrayOutputStream;
import java.io.OutputStream;
import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Marshaller;
import javax.xml.bind.Unmarshaller;
import org.apache.activemq.util.ByteArrayInputStream;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import pl.grzejszczak.marcin.camel.jaxb.generated.PlayerDetails;
@Component("PlayerDetailsConverter")
public class PlayerDetailsConverter implements Convertable<PlayerDetails> {
private static final Logger LOGGER = LoggerFactory.getLogger(PlayerDetailsConverter.class);
private final JAXBContext jaxbContext;
private final Marshaller jaxbMarshaller;
private final Unmarshaller jaxbUnmarshaller;
public PlayerDetailsConverter() throws JAXBException {
jaxbContext = JAXBContext.newInstance(PlayerDetails.class);
jaxbMarshaller = jaxbContext.createMarshaller();
jaxbMarshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);
jaxbUnmarshaller = jaxbContext.createUnmarshaller();
}
@Override
public String marshal(PlayerDetails object) {
OutputStream stream = new ByteArrayOutputStream();
try {
jaxbMarshaller.marshal(object, stream);
} catch (JAXBException e) {
LOGGER.error("Exception occured while marshalling", e);
}
return stream.toString();
}
@Override
public PlayerDetails unmarshal(String objectAsString) {
try {
return (PlayerDetails) jaxbUnmarshaller.unmarshal(new ByteArrayInputStream(objectAsString.getBytes()));
} catch (JAXBException e) {
LOGGER.error("Exception occured while marshalling", e);
}
return null;
}
}
Last but not least is the FinalListenerImpl that is listening to the inbound message from the destination queue and shuts the application.
FinalListenerImpl.java
package pl.grzejszczak.marcin.camel.manual.jms;By using the MessageConverter, after having verified if the message is of proper type, we check if the team name has already been filled in - if that is the case we are terminating the application.
import javax.jms.BytesMessage;
import javax.jms.Message;
import javax.jms.MessageListener;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jms.support.converter.MessageConverter;
import org.springframework.stereotype.Component;
import pl.grzejszczak.marcin.camel.jaxb.generated.PlayerDetails;
@Component
public class FinalListenerImpl implements MessageListener {
private static final Logger LOG = LoggerFactory.getLogger(FinalListenerImpl.class);
@Autowired
private MessageConverter messageConverter;
@Override
public void onMessage(Message message) {
if (!(message instanceof BytesMessage)) {
LOG.error("Wrong msg!");
return;
}
PlayerDetails playerDetails = null;
try {
playerDetails = (PlayerDetails) messageConverter.fromMessage(message);
if (playerDetails.getTeamName() != null) {
LOG.debug("Message already enriched! Shutting down the system");
System.exit(0);
} else {
LOG.debug("The message should have been enriched but wasn't");
System.exit(1);
}
} catch (Exception e) {
LOG.error("Exception occured", e);
}
}
}
And the logs are as follows:
2012-11-05 [main] org.springframework.context.support.ClassPathXmlApplicationContext:495 Refreshing org.springframework.context.support.ClassPathXmlApplicationContext@34fbb7cb: startup date [Mon Nov 05 21:47:00 CET 2012]; root of context hierarchy
2012-11-05 [main] org.springframework.beans.factory.xml.XmlBeanDefinitionReader:315 Loading XML bean definitions from class path resource [camel/jmsApplicationContext.xml]
2012-11-05 [main] org.springframework.beans.factory.config.PropertyPlaceholderConfigurer:177 Loading properties file from class path resource [camel/jms.properties]
2012-11-05 [main] org.springframework.beans.factory.support.DefaultListableBeanFactory:557 Pre-instantiating singletons in org.springframework.beans.factory.support.DefaultListableBeanFactory@3313beb5: defining beans [org.springframework.context.annotation.internalConfigurationAnnotationProcessor,org.springframework.context.annotation.internalAutowiredAnnotationProcessor,org.springframework.context.annotation.internalRequiredAnnotationProcessor,org.springframework.context.annotation.internalCommonAnnotationProcessor,org.springframework.context.annotation.internalPersistenceAnnotationProcessor,myRoute,AgeEnricher,ClubEnricher,PlayerDetailsConverter,finalListenerImpl,listenerImpl,playerDetailsSenderImpl,org.springframework.beans.factory.config.PropertyPlaceholderConfigurer#0,activeMQConnectionFactory,cachingConnectionFactory,origin,destination,producerTemplate,originPlayerSender,destinationPlayerSender,originListenerImpl,destinationListenerImpl,jmsOriginContainer,jmsDestinationContainer,oxmMessageConverter,marshaller,org.springframework.context.annotation.ConfigurationClassPostProcessor$ImportAwareBeanPostProcessor#0]; root of factory hierarchy
2012-11-05 [main] org.springframework.oxm.jaxb.Jaxb2Marshaller:436 Creating JAXBContext with classes to be bound [class pl.grzejszczak.marcin.camel.jaxb.generated.PlayerDetails]
2012-11-05 [main] org.springframework.context.support.DefaultLifecycleProcessor:334 Starting beans in phase 2147483647
2012-11-05 [main] org.springframework.jms.connection.CachingConnectionFactory:291 Established shared JMS Connection: ActiveMQConnection {id=ID:marcin-SR700-38535-1352148424687-1:1,clientId=null,started=false}
2012-11-05 [main] pl.grzejszczak.marcin.camel.manual.jms.PlayerDetailsSenderImpl:26 Sending [pl.grzejszczak.marcin.camel.jaxb.generated.PlayerDetails@6ae2d0b2] to topic [queue://Initial.Queue]
2012-11-05 [jmsOriginContainer-1] pl.grzejszczak.marcin.camel.manual.jms.ListenerImpl:49 Enriching the input message
2012-11-05 [jmsOriginContainer-1] pl.grzejszczak.marcin.camel.enricher.AgeEnricher:17 Enriching player [Lewandowski] with age data
2012-11-05 [jmsOriginContainer-1] pl.grzejszczak.marcin.camel.enricher.ClubEnricher:16 Enriching player [Lewandowski] with club data
2012-11-05 [jmsOriginContainer-1] pl.grzejszczak.marcin.camel.manual.jms.ListenerImpl:53 Enriched text message: [<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<PlayerDetails>
<Name>Robert</Name>
<Surname>Lewandowski</Surname>
<Position>ATT</Position>
<Age>19</Age>
<TeamName>Borussia Dortmund</TeamName>
</PlayerDetails>
]
2012-11-05 [jmsOriginContainer-1] pl.grzejszczak.marcin.camel.manual.jms.PlayerDetailsSenderImpl:26 Sending [pl.grzejszczak.marcin.camel.jaxb.generated.PlayerDetails@3dca1588] to topic [queue://Routed.Queue]
2012-11-05 [jmsDestinationContainer-1] pl.grzejszczak.marcin.camel.manual.jms.FinalListenerImpl:35 Message already enriched! Shutting down the system
This is how thanks to the Spring JMS module and the JAXB library you can easilly create JMS listeners, senders and message convertors for the XML messages.
Spring AOP in security - controlling creation of UI components via aspects
The following post will show how in one of the projects that I took part in we used Spring's AOP to introduce some security related functionalities. The concept was such that in order for the user to see some UI components he needed to have a certain level of security privillages. If that requirement was not met then the UIComponent was not presented. Let's take a look at the project structure:
Then there were also the aopApplicationContext.xml :
<?xml version="1.0" encoding="UTF-8"?>Now let's take a look at the most interesting lines of the Spring's application context.
<beans xmlns="https://www.springframework.org/schema/beans"
xmlns:context="https://www.springframework.org/schema/context"
xmlns:xsi="https://www.w3.org/2001/XMLSchema-instance"
xmlns:aop="https://www.springframework.org/schema/aop"
xsi:schemaLocation="https://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans-3.0.xsd
https://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context-3.0.xsd
https://www.springframework.org/schema/tx https://www.springframework.org/schema/tx/spring-tx-3.0.xsd
https://www.springframework.org/schema/util https://www.springframework.org/schema/util/spring-util-3.1.xsd
https://www.springframework.org/schema/aop https://www.springframework.org/schema/aop/spring-aop-3.0.xsd">
<aop:aspectj-autoproxy />
<context:annotation-config />
<context:component-scan base-package="pl.grzejszczak.marcin.aop">
<context:exclude-filter type="annotation" expression="org.aspectj.lang.annotation.Aspect"/>
</context:component-scan>
<bean class="pl.grzejszczak.marcin.aop.interceptor.SecurityInterceptor" factory-method="aspectOf"/>
</beans>
First we have all the required schemas - I don't think that this needs to be explained in more depth.
Then we have:
<aop:aspectj-autoproxy/>
which enables the @AspectJ support.
Next there is the
<context:annotation-config />
<context:component-scan base-package="pl.grzejszczak.marcin.aop">
<context:exclude-filter type="annotation" expression="org.aspectj.lang.annotation.Aspect"/>
</context:component-scan>
first we are turning on Spring configuration via annotations. Then deliberatly we exclude aspects from being initialized as beans by Spring itself. Why? Because...
<bean class="pl.grzejszczak.marcin.aop.interceptor.SecurityInterceptor" factory-method="aspectOf"/>
we want to create the aspect by ourselves and provide the factory-method="aspectOf" . By doing so our aspect will be included in the autowiring process of our beans - thus all the fields annotated with the @Autowired annotation will get the beans injected.
Now let's move on to the code:
UserServiceImpl.java
package pl.grzejszczak.marcin.aop.service;
import org.springframework.stereotype.Service;
import pl.grzejszczak.marcin.aop.type.Role;
import pl.grzejszczak.marcin.aop.user.UserHolder;
@Service
public class UserServiceImpl implements UserService {
private UserHolder userHolder;
@Override
public UserHolder getCurrentUser() {
return userHolder;
}
@Override
public void setCurrentUser(UserHolder userHolder) {
this.userHolder = userHolder;
}
@Override
public Role getUserRole() {
if (userHolder == null) {
return null;
}
return userHolder.getUserRole();
}
}
The class UserServiceImpl is immitating a service that would get the current user information from the db or from the current application context.
UserHolder.java
package pl.grzejszczak.marcin.aop.user;
import pl.grzejszczak.marcin.aop.type.Role;
public class UserHolder {
private Role userRole;
public UserHolder(Role userRole) {
this.userRole = userRole;
}
public Role getUserRole() {
return userRole;
}
public void setUserRole(Role userRole) {
this.userRole = userRole;
}
}
This is a simple holder class that holds information about current user Role.
Role.java
package pl.grzejszczak.marcin.aop.type;
public enum Role {
ADMIN("ADM"), WRITER("WRT"), GUEST("GST");
private String name;
private Role(String name) {
this.name = name;
}
public static Role getRoleByName(String name) {
for (Role role : Role.values()) {
if (role.name.equals(name)) {
return role;
}
}
throw new IllegalArgumentException("No such role exists [" + name + "]");
}
public String getName() {
return this.name;
}
@Override
public String toString() {
return name;
}
}
Role is an enum that defines a role for a person being an Admin, Writer or a Guest.
UIComponent.java
package pl.grzejszczak.marcin.aop.ui;
public abstract class UIComponent {
protected String componentName;
protected String getComponentName() {
return componentName;
}
}
An abstraction over concrete implementations of some UI components.
SomeComponentForAdminAndGuest.java
package pl.grzejszczak.marcin.aop.ui;
import pl.grzejszczak.marcin.aop.annotation.SecurityAnnotation;
import pl.grzejszczak.marcin.aop.type.Role;
@SecurityAnnotation(allowedRole = { Role.ADMIN, Role.GUEST })
public class SomeComponentForAdminAndGuest extends UIComponent {
public SomeComponentForAdminAndGuest() {
this.componentName = "SomeComponentForAdmin";
}
public static UIComponent getComponent() {
return new SomeComponentForAdminAndGuest();
}
}
This component is an example of a UI component extention that can be seen only by users who have roles of Admin or Guest.
SecurityAnnotation.java
package pl.grzejszczak.marcin.aop.annotation;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import pl.grzejszczak.marcin.aop.type.Role;
@Retention(RetentionPolicy.RUNTIME)
public @interface SecurityAnnotation {
Role[] allowedRole();
}
Annotation that defines a roles that can have this component created.
UIFactoryImpl.java
package pl.grzejszczak.marcin.aop.ui;
import org.apache.commons.lang.NullArgumentException;
import org.springframework.stereotype.Component;
@Component
public class UIFactoryImpl implements UIFactory {
@Override
public UIComponent createComponent(Class<? extends UIComponent> componentClass) throws Exception {
if (componentClass == null) {
throw new NullArgumentException("Provide class for the component");
}
return (UIComponent) Class.forName(componentClass.getName()).newInstance();
}
}
A factory class that given the class of an object that extends UIComponent returns a new instance of the given UIComponent.
SecurityInterceptor.java
package pl.grzejszczak.marcin.aop.interceptor;
import java.lang.annotation.Annotation;
import java.lang.reflect.AnnotatedElement;
import java.util.Arrays;
import java.util.List;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import pl.grzejszczak.marcin.aop.annotation.SecurityAnnotation;
import pl.grzejszczak.marcin.aop.service.UserService;
import pl.grzejszczak.marcin.aop.type.Role;
import pl.grzejszczak.marcin.aop.ui.UIComponent;
@Aspect
public class SecurityInterceptor {
private static final Logger LOGGER = LoggerFactory.getLogger(SecurityInterceptor.class);
public SecurityInterceptor() {
LOGGER.debug("Security Interceptor created");
}
@Autowired
private UserService userService;
@Pointcut("execution(pl.grzejszczak.marcin.aop.ui.UIComponent pl.grzejszczak.marcin.aop.ui.UIFactory.createComponent(..))")
private void getComponent(ProceedingJoinPoint thisJoinPoint) {
}
@Around("getComponent(thisJoinPoint)")
public UIComponent checkSecurity(ProceedingJoinPoint thisJoinPoint) throws Throwable {
LOGGER.info("Intercepting creation of a component");
Object[] arguments = thisJoinPoint.getArgs();
if (arguments.length == 0) {
return null;
}
Annotation annotation = checkTheAnnotation(arguments);
boolean securityAnnotationPresent = (annotation != null);
if (securityAnnotationPresent) {
boolean userHasRole = verifyRole(annotation);
if (!userHasRole) {
LOGGER.info("Current user doesn't have permission to have this component created");
return null;
}
}
LOGGER.info("Current user has required permissions for creating a component");
return (UIComponent) thisJoinPoint.proceed();
}
/**
* Basing on the method's argument check if the class is annotataed with
* {@link SecurityAnnotation}
*
* @param arguments
* @return
*/
private Annotation checkTheAnnotation(Object[] arguments) {
Object concreteClass = arguments[0];
LOGGER.info("Argument's class - [{}]", new Object[] { arguments });
AnnotatedElement annotatedElement = (AnnotatedElement) concreteClass;
Annotation annotation = annotatedElement.getAnnotation(SecurityAnnotation.class);
LOGGER.info("Annotation present - [{}]", new Object[] { annotation });
return annotation;
}
/**
* The function verifies if the current user has sufficient privilages to
* have the component built
*
* @param annotation
* @return
*/
private boolean verifyRole(Annotation annotation) {
LOGGER.info("Security annotation is present so checking if the user can use it");
SecurityAnnotation annotationRule = (SecurityAnnotation) annotation;
List<Role> requiredRolesList = Arrays.asList(annotationRule.allowedRole());
Role userRole = userService.getUserRole();
return requiredRolesList.contains(userRole);
}
}
This is the aspect defined at the pointcut of executing a function createComponent of the UIFactory interface. Inside the Around advice there is the logic that first checks what kind of an argument has been passed to the method createComponent (for example SomeComponentForAdminAndGuest.class). Next it is checking if this class is annotated with SecurityAnnotation and if that is the case it checks what kind of Roles are required to have the component created. Afterwards it checks if the current user (from UserService to UserHolder's Roles) has the required role to present the component. If that is the case thisJoinPoint.proceed() is called which in effect returns the object of the class that extends UIComponent.
Now let's test it - here comes the SpringJUnit4ClassRunner
AopTest.java
package pl.grzejszczak.marcin.aop;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import pl.grzejszczak.marcin.aop.service.UserService;
import pl.grzejszczak.marcin.aop.type.Role;
import pl.grzejszczak.marcin.aop.ui.SomeComponentForAdmin;
import pl.grzejszczak.marcin.aop.ui.SomeComponentForAdminAndGuest;
import pl.grzejszczak.marcin.aop.ui.SomeComponentForGuest;
import pl.grzejszczak.marcin.aop.ui.SomeComponentForWriter;
import pl.grzejszczak.marcin.aop.ui.UIFactory;
import pl.grzejszczak.marcin.aop.user.UserHolder;
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = { "classpath:aopApplicationContext.xml" })
public class AopTest {
@Autowired
private UIFactory uiFactory;
@Autowired
private UserService userService;
@Test
public void adminTest() throws Exception {
userService.setCurrentUser(new UserHolder(Role.ADMIN));
Assert.assertNotNull(uiFactory.createComponent(SomeComponentForAdmin.class));
Assert.assertNotNull(uiFactory.createComponent(SomeComponentForAdminAndGuest.class));
Assert.assertNull(uiFactory.createComponent(SomeComponentForGuest.class));
Assert.assertNull(uiFactory.createComponent(SomeComponentForWriter.class));
}
}
And the logs:
pl.grzejszczak.marcin.aop.interceptor.SecurityInterceptor:26 Security Interceptor created
pl.grzejszczak.marcin.aop.interceptor.SecurityInterceptor:38 Intercepting creation of a component
pl.grzejszczak.marcin.aop.interceptor.SecurityInterceptor:48 Argument's class - [[class pl.grzejszczak.marcin.aop.ui.SomeComponentForAdmin]]
pl.grzejszczak.marcin.aop.interceptor.SecurityInterceptor:54 Annotation present - [@pl.grzejszczak.marcin.aop.annotation.SecurityAnnotation(allowedRole=[ADM])]
pl.grzejszczak.marcin.aop.interceptor.SecurityInterceptor:57 Security annotation is present so checking if the user can use it
pl.grzejszczak.marcin.aop.interceptor.SecurityInterceptor:70 Current user has required permissions for creating a component
pl.grzejszczak.marcin.aop.interceptor.SecurityInterceptor:38 Intercepting creation of a component
pl.grzejszczak.marcin.aop.interceptor.SecurityInterceptor:48 Argument's class - [[class pl.grzejszczak.marcin.aop.ui.SomeComponentForAdminAndGuest]]
pl.grzejszczak.marcin.aop.interceptor.SecurityInterceptor:54 Annotation present - [@pl.grzejszczak.marcin.aop.annotation.SecurityAnnotation(allowedRole=[ADM, GST])]
pl.grzejszczak.marcin.aop.interceptor.SecurityInterceptor:57 Security annotation is present so checking if the user can use it
pl.grzejszczak.marcin.aop.interceptor.SecurityInterceptor:70 Current user has required permissions for creating a component
pl.grzejszczak.marcin.aop.interceptor.SecurityInterceptor:38 Intercepting creation of a component
pl.grzejszczak.marcin.aop.interceptor.SecurityInterceptor:48 Argument's class - [[class pl.grzejszczak.marcin.aop.ui.SomeComponentForGuest]]
pl.grzejszczak.marcin.aop.interceptor.SecurityInterceptor:54 Annotation present - [@pl.grzejszczak.marcin.aop.annotation.SecurityAnnotation(allowedRole=[GST])]
pl.grzejszczak.marcin.aop.interceptor.SecurityInterceptor:57 Security annotation is present so checking if the user can use it
pl.grzejszczak.marcin.aop.interceptor.SecurityInterceptor:66 Current user doesn't have permission to have this component created
pl.grzejszczak.marcin.aop.interceptor.SecurityInterceptor:38 Intercepting creation of a component
pl.grzejszczak.marcin.aop.interceptor.SecurityInterceptor:48 Argument's class - [[class pl.grzejszczak.marcin.aop.ui.SomeComponentForWriter]]
pl.grzejszczak.marcin.aop.interceptor.SecurityInterceptor:54 Annotation present - [@pl.grzejszczak.marcin.aop.annotation.SecurityAnnotation(allowedRole=[WRT])]
pl.grzejszczak.marcin.aop.interceptor.SecurityInterceptor:57 Security annotation is present so checking if the user can use it
pl.grzejszczak.marcin.aop.interceptor.SecurityInterceptor:66 Current user doesn't have permission to have this component created
The unit test shows that for given Admin role only first two components get created whereas for the two others nulls are returned (due to the fact that user doesn't have proper rights).
That is how in our project we used Spring's AOP to create a simple framework that would check if the user can have the given component created or not. Thanks to this after having programmed the aspects one doesn't have to remember about writing any security related code since it will be done for him.
If you have any suggestions related to this post please feel free to comment it :)
Mockito InvocationOnMock - checking an argument
Hi!
In one of my projects we had a very interesting situation in terms of testing. We couldn't mock an invocation of a static method due to the fact that PowerMock was not allowed to be used so there were plenty of objects and dependencies being initialized. What is more we were using a custom made dependency injection system that had a possibility of injecting a mock.
The problem was such that during a test we wanted to assert whether one of the objects was in a very precise state. This object was created using the new operator so we couldn't mock it (again no PowerMock allowed). Fortunately this object got passed to a method of an object that we could mock...
As presented below timeConsumingExternalService is an object that we could mock via the custom dependency injection system whereas the SomePojo class is an object whose state we would like to verify.
timeConsumingExternalService.processSomeObject(new SomePojo("name", "surname", 1, 1.0));
So what we did was that in our mock to which the object got passed we created a new Answer (the same Answer that I spoke of here). Due to which we could access the InvocationOnMock and the arguments passed to the method as such.
Mockito.doAnswer(new Answer<Object>() {
public Object answer(InvocationOnMock invocation) throws Throwable {
Object[] object = invocation.getArguments();
if (object.length > 0) {
SomePojo somePojo = (SomePojo) object[0];
Assert.assertEquals("name", somePojo.getName());
LOGGER.debug("Names are equal");
Assert.assertEquals("surname", somePojo.getSurname());
LOGGER.debug("Surnames are equal");
Assert.assertTrue(1 == somePojo.getIntValue());
LOGGER.debug("Ints are equal");
Assert.assertTrue(1.0 == somePojo.getDoubleValue());
LOGGER.debug("Doubles are equal");
LOGGER.debug("Object being an argument of the function [" + String.valueOf(somePojo) + "]");
}
return null;
}
}).when(timeConsumingExternalServiceMock).processSomeObject(Mockito.any(SomePojo.class));
Of course the logs regarding the equalities are unnecessary since if they wouldn't be equal we would have an assertion exception - I left them for the purpose of this post.
And in the logs we can find:
pl.grzejszczak.marcin.ServiceIntegrationTest:48 Names are equal
pl.grzejszczak.marcin.ServiceIntegrationTest:50 Surnames are equal
pl.grzejszczak.marcin.ServiceIntegrationTest:52 Ints are equal
pl.grzejszczak.marcin.ServiceIntegrationTest:54 Doubles are equal
pl.grzejszczak.marcin.ServiceIntegrationTest:56 Object being an argument of the function [SomePojo [name=name, surname=surname, intValue=1, doubleValue=1.0]]
So in this way something that seems impossible to be verified can get verified :)
Update!
Thanks to Holger's suggestion I took a look at the ArgumentCaptor object and that is true that it is an elegant solution to retrieve information about the arguments executed on a method. Where InvocationOnMock can give you much more information and possibilities (for instance regarding the method being executed or just execute the real method) for this particular case a much more elegant, easier and faster way of dealing with the issue would be:
//service that executes the external service
executorService.execute(someTask);
final ArgumentCaptor<SomePojo> argumentCaptor = ArgumentCaptor.forClass(SomePojo.class);
Mockito.verify(timeConsumingExternalServiceMock).processSomeObject(argumentCaptor.capture());
SomePojo somePojo = argumentCaptor.getValue();
Assert.assertEquals("name", somePojo.getName());
LOGGER.debug("Names are equal");
Assert.assertEquals("surname", somePojo.getSurname());
LOGGER.debug("Surnames are equal");
Assert.assertTrue(1 == somePojo.getIntValue());
LOGGER.debug("Ints are equal");
Assert.assertTrue(1.0 == somePojo.getDoubleValue());
LOGGER.debug("Doubles are equal");
The logs:
pl.grzejszczak.marcin.junit.SomeTask:26 Before processing an object
pl.grzejszczak.marcin.junit.SomeTask:28 After processing an object
pl.grzejszczak.marcin.ServiceIntegrationTest:75 Names are equal
pl.grzejszczak.marcin.ServiceIntegrationTest:77 Surnames are equal
pl.grzejszczak.marcin.ServiceIntegrationTest:79 Ints are equal
pl.grzejszczak.marcin.ServiceIntegrationTest:81 Doubles are equal
Thanks again Holger!