Setup Spring Security with Active Directory LDAP in Spring Boot Web Application

This post illustrates how to set up Spring Security in Spring Boot configuration with Active Directory LDAP for a Spring MVC web application. I will also show what needs to be configured for the embedded tomcat to accept HTTPS.

Spring Security with LDAP

To configure Spring Security in Spring Boot, add the following Configuration class to your project. Note the use of annotation @EnableWebMvcSecurity. The configuration class extends the WebSecurityConfigurerAdapter class in Spring Security. More information can be found in the Spring Security Reference here.

@Configuration
@EnableWebMvcSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

     @Value("${ldap.domain}")
     private String DOMAIN;

     @Value("${ldap.url}")
     private String URL;

     @Value("${http.port}")
     private int httpPort;

     @Value("${https.port}")
     private int httpsPort;

     @Override
     protected void configure(HttpSecurity http) throws Exception {
          /*
           * Set up your spring security config here. For example...
          */
          http.authorizeRequests().anyRequest().authenticated().and().formLogin().loginUrl("/login").permitAll();
          /*
           * Use HTTPs for ALL requests
          */
          http.requiresChannel().anyRequest().requiresSecure();
          http.portMapper().http(httpPort).mapsTo(httpsPort);
     }

     @Override
     protected void configure(AuthenticationManagerBuilder authManagerBuilder) throws Exception {
          authManagerBuilder.authenticationProvider(activeDirectoryLdapAuthenticationProvider()).userDetailsService(userDetailsService());
     }

     @Bean
     public AuthenticationManager authenticationManager() {
          return new ProviderManager(Arrays.asList(activeDirectoryLdapAuthenticationProvider()));
     }
     @Bean
     public AuthenticationProvider activeDirectoryLdapAuthenticationProvider() {
          ActiveDirectoryLdapAuthenticationProvider provider = new ActiveDirectoryLdapAuthenticationProvider(DOMAIN, URL);
          provider.setConvertSubErrorCodesToExceptions(true);
          provider.setUseAuthenticationRequestCredentials(true);
          return provider;
     }
}

Add HTTPS connector for embedded Tomcat in Spring Boot

Now that Spring Security is set up, you need to update the web server to accept requests from HTTPS. To do that using the embedded Tomcat server in Spring Boot, add the following EmbeddedServletContainerCustomizer bean to the application configuration as shown below. Note I am using anonymous inner classes here instead of lambda expression as I see in other examples for Java 7 compatibility. You will need a keystore file for this to work.

@Bean
EmbeddedServletContainerCustomizer containerCustomizer (

     @Value("${https.port}") final int port, 
     @Value("${keystore.file}") Resource keystoreFile,
     @Value("${keystore.alias}") final String alias, 
     @Value("${keystore.password}") final String keystorePass,
     @Value("${keystore.type}") final String keystoreType) throws Exception {
          final String absoluteKeystoreFile = keystoreFile.getFile().getAbsolutePath();
          return new EmbeddedServletContainerCustomizer() {
               public void customize(ConfigurableEmbeddedServletContainer container) {
                    TomcatEmbeddedServletContainerFactory tomcat = (TomcatEmbeddedServletContainerFactory) container;
                    tomcat.addConnectorCustomizers(new TomcatConnectorCustomizer() {
                         public void customize(Connector connector) {
                              connector.setPort(port);
                              connector.setSecure(true);
                              connector.setScheme("https");
                              Http11NioProtocol proto = (Http11NioProtocol) connector.getProtocolHandler();
                              proto.setSSLEnabled(true);
                              proto.setKeystoreFile(absoluteKeystoreFile);
                              proto.setKeyAlias(alias);
                              proto.setKeystorePass(keystorePass);
                              proto.setKeystoreType(keystoreType);
                        }
               });
           }
     };
 }

 

Getting Start with Spring Boot Configurations

Spring Boot allows development of Spring applications with minimum configuration. This is particular useful for developing microservices. This blog post will demonstrate a few things that may help in understanding how Spring Boot does its job of auto configuring a Spring application.

Auto-Configuration

So what has been configured?

The main feature of Spring Boot is its ability to automatically configuration the Spring application based on its included jar files. So the first thing you may want to look at is what have been configured for you. This can be done by running the application with the debug flag either by adding “–debug” to the command line or JVM argument “-Ddebug“. You will then see the “Auto Configuration Report” displayed in your console like below:

=========================
AUTO-CONFIGURATION REPORT
=========================

Positive matches:
—————–

AopAutoConfiguration
– @ConditionalOnClass classes found: org.springframework.context.annotation.EnableAspectJAutoProxy,org.aspectj.lang.annotation.Aspect,org.aspectj.lang.reflect.Advice (OnClassCondition)
– SpEL expression on org.springframework.boot.autoconfigure.aop.AopAutoConfiguration: ${spring.aop.auto:true} (OnExpressionCondition)

AopAutoConfiguration.JdkDynamicAutoProxyConfiguration
– SpEL expression on org.springframework.boot.autoconfigure.aop.AopAutoConfiguration$JdkDynamicAutoProxyConfiguration: !${spring.aop.proxyTargetClass:false} (OnExpressionCondition)

// rest omitted…

Negative matches:
—————–

ActiveMQAutoConfiguration
– required @ConditionalOnClass classes not found: javax.jms.ConnectionFactory,org.apache.activemq.ActiveMQConnectionFactory (OnClassCondition)

// rest omitted…

The report should give you an indication of what have been configured for you. It depends mainly on what jar files you have included in your project dependencies.

Excluding an auto configuration

Spring Boot is designed so that you can gradually replaced the auto-configuration as needed. To exclude an auto configuration, use the exclude attribute of the @EnableAutoConfiguration annotation as below:

@Configuration
@ComponentScan
@EnableAutoConfiguration(exclude = ProcessEngineAutoConfiguration.class)
@ImportResource("classpath:/activiti.xml")
public class Application {

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

I am using the Activiti Spring Boot module here. Note instead of relying on its auto configuration setup, I have included the activiti.xml configuration file using the @ImportResource annotation to manually configure the process engine used by Activiti.

Configuration properties

Application properties, e.g. JDBC connection string, are to be set in the application.properties file in the classpath. Profile specific properties should be included in a separate application-<profile>.properties file located at the same directory of the application.properties file.

A list of commonly used property keys can be found in the Reference Guide (here). For example, to change the embedded web server port to 8181, add the following line the the application.properties file

### application.properties

server.port=8181

Logging

You may configure Spring Boot to use logging framework of your choice. But first, it may be useful to configure the logger properties and understand what has been configured for you. To do this, add the property of the format logging.level.<package name>=<level> to your application.properties file. For example, to display debug messages for Hibernate, add the following lilne:

logging.level.org.hibernate=DEBUG

That’s it. The above has helped me to get started with Spring Boot, to understand how auto configuration works. The Reference Guide provides a comprehensive documentation of the framework and various how-tos. There are also many tutorials and blog articles around for reference.

 

Including Field Value In Validation Message Using Spring Validation framework for JSR-303

This blog post demonstrates how to include the value of the field in the error message when using Spring Validation framework support for JSR-303. This has been made possible in Bean Validation API 1.1 with the new support of error message interpolation using EL expression. In particular, the use of the parameter “validatedValue” in the message. This post is inspired by the blog “Better Error Messages With Bean Validation 1.1 In Spring MVC Application” and will focus on the Spring Validation framework alone and demonstrate alternative ways to setup message keys using validateValue

Application Setup

I am using Spring 4 and Hibernate Validator 5.1.1 in the example here.

Spring Configuration

Nothing special with the spring configuration file (app-config.xml) here. The messageAccessor bean is used by the test class to get the validation error messages.

<?xml version=”1.0″ encoding=”UTF-8″?>
<beans xmlns=”http://www.springframework.org/schema/beans&#8221;
xmlns:xsi=”http://www.w3.org/2001/XMLSchema-instance&#8221; xmlns:mvc=”http://www.springframework.org/schema/mvc&#8221;
xmlns:context=”http://www.springframework.org/schema/context&#8221;
xsi:schemaLocation=”

http://www.springframework.org/schema/beans

http://www.springframework.org/schema/beans/spring-beans-4.0.xsd

http://www.springframework.org/schema/context

http://www.springframework.org/schema/context/spring-context-4.0.xsd”&gt;

<!– Creates the JSR-303 Validator –>
<bean id=”validator” class=”org.springframework.validation.beanvalidation.LocalValidatorFactoryBean” >
<property name=”validationMessageSource” ref=”messageSource”/>
</bean>

<bean id=”messageSource” class=”org.springframework.context.support.ResourceBundleMessageSource”>
<property name=”basenames”>
<list>
<value>messages</value>
</list>
</property>
</bean>

<bean id=”messageAccessor” class=”org.springframework.context.support.MessageSourceAccessor”>
<constructor-arg index=”0″ ref=”messageSource”/>
</bean>

</beans>

Order Class

Let say we have the following class we need to validate against. For simplicity, all the 3 fields have to be of at least 10 characters. The javax validation annotation @Size(min = 10) is hence used.


public class Order {

@Size(min = 10, message = “{Size.order.customerName.custom} ${validatedValue}”)
private String customerName;

@Size(min = 10, message =”phone number entered [${validatedValue}] is invalid. It must have at least {min} digits”)
private String phoneNumber;

@Size(min = 10)
private String address;

// getter and setter omitted here …

messages.properties file

The error messages are defined in the messages.properties file (shown below) and are used by the validator to resolve the message keys against.

javax.validation.constraints.Size.message=Invalid size for input: ${validatedValue}
Size.order.customerName.custom=Invalid customer name:

Test Class

Now we are ready to write some tests

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(“/spring/app-config.xml”)
public class OrderValidationTest {

@Autowired
private Validator validator;

@Autowired
private MessageSourceAccessor messageSourceAccessor;

@Test
public void testValidateOrder() {

Order order = new Order();
order.setCustomerName(“a”);
order.setPhoneNumber(“1234″);
order.setAddress(“b”);
BindingResult errors = new BeanPropertyBindingResult(order, “order”);
validator.validate(order, errors);
assertEquals(“Invalid customer name: a”, getValidationErrorMessage(errors, “customerName”));
assertEquals(“phone number entered [1234] is invalid. It must have at least 10 digits”, getValidationErrorMessage(errors, “phoneNumber”));
assertEquals(“Invalid size for input: b”, getValidationErrorMessage(errors, “address”));

}

private String getValidationErrorMessage(BindingResult result, String field) {

if (result.hasErrors()) {
FieldError fieldError = result.getFieldError(field);
return messageSourceAccessor.getMessage(fieldError);
}
return “”;

}

}

The example classes above demonstrate 3 different ways to include the value of the invalid field in the error message, all using the validatedValue parameter supported by Bean Validation 1.1 API.

1. Update default message for the validator

First option is to modify the default message key used by the validation annotation as shown in the field address in the messages.properties file as shown below:

@Size(min = 10)
private String address;

// messages.properties

javax.validation.constraints.Size.message=Invalid size for input: ${validatedValue}

2.  Add message attribute to the validation annotation

To include validatedValue only for certain field in a class, add message attribute to the validator annotation:

@Size(min = 10, message =”phone number entered [${validatedValue}] is invalid. It must have at least {min} digits”)
private String phoneNumber;

3. Add custom message key in the message attribute

It is possible to use message key by enclosing with {} if you do not want to hard coded message text in the class:

@Size(min = 10, message = “{Size.order.customerName.custom} ${validatedValue}”)
private String customerName;

// messages.properties

Size.order.customerName.custom=Invalid customer name:

You cannot use the same key as expected by Spring, i.e. Size.order.customerName. Note also you cannot just add “${validatedValue}” in the text in messages.properties file.

That’s it. Hope this post helps.

Resources

  1. Hibernate Validator 5.1. documentation – See this chapter for more details on message interpolation.

Set up a full broker for Spring 4 STOMP over WebSocket messaging using ActiveMQ

Spring 4 websocket comes with a built-in “simple” broker for handling messaging in memory. In this blog, I will demonstrate how to configure Spring 4 to use a “full” broker, i.e. ActiveMQ, to support STOMP over WebSocket messaging.

1. Enable ActiveMQ for STOMP

First, we need to enable STOMP protocol support in ActiveMQ. This can be done by adding the connector in the activemq.xml file as shown below.

 <transportConnectors>
       <transportConnector name="openwire" uri="tcp://0.0.0.0:61616"/> 
       <transportConnector uri="stomp://localhost:61613"/>
 </transportConnectors>

You may also setup security as mentioned in the ActiveMQ documentation here.

2. Add reactor-tcp jar files

Add to following dependencies to the pom.xml file:

 <!-- Reactor for websocket relay to MQ-->
 <dependency>
      <groupId>org.projectreactor</groupId>
      <artifactId>reactor-core</artifactId>
      <version>1.0.0.RELEASE</version>
 </dependency>
 <dependency>
      <groupId>org.projectreactor</groupId>
      <artifactId>reactor-tcp</artifactId>
      <version>1.0.0.RELEASE</version>
 </dependency>

Or download the jar files reactor-core-1.0.0.RELEASE.jar and reactor-tcp-1.0.0.RELEASE.jar into your classpath

3. Update Spring WebSocket config

Replace the simple broker config below

 <websocket:message-broker application-destination-prefix="/app">
       <websocket:stomp-endpoint path="/hello">
             <websocket:sockjs/>
       </websocket:stomp-endpoint>
       <websocket:simple-broker prefix="/topic,/queue"/>
 </websocket:message-broker>

with a full broker:

 <websocket:message-broker application-destination-prefix="/app">
       <websocket:stomp-endpoint path="/hello">
            <websocket:sockjs/>
       </websocket:stomp-endpoint>
       <websocket:stomp-broker-relay prefix="/topic,/queue"
           relay-host="localhost" relay-port="61613"
           heartbeat-send-interval="20000" heartbeat-receive-interval="20000"/>
 </websocket:message-broker>

If you setup security in ActiveMQ, you will also need to include the attributes client-login, client-passcode, system-login and system-passcode for connecting to  the broker on behalf of the client and the application respectively. By default, they are set as “guest”

Putting in altogether

That’s it. Now to test it out. first fire off ActiveMQ, you should see in the logger messages something like this:

INFO | Listening for connections at: stomp://messageserver:61613
INFO | Connector stomp://localhost:61613 Started

Now start the web server, I am using jetty 9. You should see the following logger message from reactor-tcp:

INFO : 12 Apr 2014 10:47:13,023 (reactor-tcp-io-3) reactor.tcp.netty.NettyTcpClient - CONNECT: [id: 0x71a8bfb1, /127.0.0.1:49617 =>       localhost/127.0.0.1:61613]
 ...
[INFO] Started Jetty Server

Now we are ready to send some messages to the full broker. I use the hello world application in my previous blog to send a message to the topic “greetings”. You should then see the topic created in the ActiveMQ admin console, e.g. http://localhost:8161/admin/topics.jsp.

Setup Spring transactions for MySQL Replication

This post describes how to setup Spring transaction to connect to MySQL database with Replication to direct all write operations to the master and read operations to both master and slaves.

Database setup

The easiest way to setup MySQL database with replication for testing is via Amazon AWS. Create a RDS instance with MySQL as a master and then create a Read Replica from the master. Note AWS uses native MySQL replication to propagate database changes from  the master to the slaves.

JDBC connection

To connect to MySQL Replication using JDBC, made the following 2 changes to your spring config:

  1. Replace the jdbc driver class, e.g. com.mysql.jdbc.Driver, with com.mysql.jdbc.ReplicationDriver.
  2. Modify jdbc connection string to format

             jdbc:mysql:replication://<master db url>,<slave 1 db url>/<dbname>

See MySQL documentation here for list of configuration properties that can be appended to the jdbc URL.

Note the ReplicationDriver wraps one read and one write jdbc connection and can be used transparently with jdbc connection pools such as cp30.

Spring transaction

Use the readOnly attribute of the @Transaction annotation of Spring to direct a transaction to either the master or slave.

For write operations, use @Transactional(readOnly = false) and the database operations will go to the master only

For read only operations, use @Transactional(readOnly = true) and the database operations can go to the slave.

Note:

  1. There are a few old articles on the web indicating the readOnly attribute is ignored. This seems to be outdated and the attribute is working as expected in Spring 3. I am using Spring 3.2.

To verify the setup, I have the following Spring test class:

@RunWith(SpringJUnit4ClassRunner.class)
@TransactionConfiguration(transactionManager="transactionManager", defaultRollback=true)
@ContextConfiguration("classpath:spring/app-config-test.xml")
@Transactional(readOnly = true)
@ActiveProfiles(profiles={"aws"})
public class ProductRepositoryImplTest {
@Autowired
 private SessionFactory sessionFactory;
@Autowired
 @Qualifier("productRepository")
 private IProductRepository repository;
// ... setup details omitted here
@Transactional(readOnly = false)
 @Test
 @Repeat(value = 100)
 public void testReplicationWrite() {
      repository.create(createEntity());
 }
 @Transactional(readOnly = true)
 @Test(expected = GenericJDBCException.class)
 public void testReplicationWriteFail() {
      repository.create(createEntity());
 }
 @Transactional(readOnly = true)
 @Test
 @Repeat(value = 100)
 public void testReplicationRead() {
      repository.findProductByName(RandomStringUtils.randomAlphabetic(10));
 }
}

Note:

  1. The test class above tests the productRepository bean which implements standard CRUD operations (details omitted here) for the entity Product.
  2. The first test method testReplicationWrite() will pass as the readOnly attribute of the @Transactional annotation is set to false.
  3. The second test method testReplicationWriteFail() will throw a GenericJDBCException, as expected by the test method. This confirms that the readOnly attribute works by sending the database operation to the slave and hence the exception. If you remove the “expected = GenericJDBCException.class”, the test will fail with the following error: “org.hibernate.exception.GenericJDBCException: Connection is read-only. Queries leading to data modification are not allowed”
  4. You can also verify on the MySQL master and slave by using the “show processlist” command while the tests are running. The @Repeat annotation runs the tests multiple (100) times to keep the connection process running. Of course, you can also enable the query log for this.

Using Spring 4 WebSocket, sockJS and Stomp support to implement two way server client communication

One exciting new feature of Spring 4 is the support for WebSocket, SockJS and STOMP messaging. This allows two way communication between the server and its clients in a Spring MVC web application using the standard point-to-point and publish-subscribe messaging protocols. In this post, I will demonstrate how to set up a basic boilerplate project to start using this new feature. It is in part based on this article.

Maven Setup

First we need to add the Spring messaging modules in the POM file:

<dependency>
 <groupId>org.springframework</groupId>
 <artifactId>spring-messaging</artifactId>
 <version>4.0.0.RELEASE</version>
 </dependency>
 <dependency>
 <groupId>org.springframework</groupId>
 <artifactId>spring-websocket</artifactId>
 <version>4.0.0.RELEASE</version>
 </dependency>

Spring MVC Configuration

Next, we need to add the message broker config to the Spring MVC config XML file.

<beans
 ...
 xmlns:websocket="http://www.springframework.org/schema/websocket"
 xsi:schemaLocation="
 ...

http://www.springframework.org/schema/websocket

 http://www.springframework.org/schema/websocket/spring-websocket-4.0.xsd">
<websocket:message-broker application-destination-prefix="/app">
       <websocket:stomp-endpoint path="/hello">
            <websocket:sockjs/>
       </websocket:stomp-endpoint>
       <websocket:simple-broker prefix="/topic"/>
</websocket:message-broker>
<!-- Other MVC config omitted here-->

The main thing here is the set up of the message broker for handling the message exchange between the server and its clients. This is done via the <message-broker> and its child tags. The tag <websocket:simple-broker> indicates we are using in-memory message broker.

It is easy to understand together with the server and client codes so I will include them below first before attempting to give a bit more explanations by cross-referencing the client and server codes.

Spring MVC Controller

Below is my Spring MVC Controller

 @Controller
 public class MessageController {
      @MessageMapping("/hello")
      @SendTo("/topic/greetings")
      public Greeting greeting(HelloMessage message) throws Exception {
           return new Greeting("Hello, " + message.getName() + "!");
     }
}

The method argument HelloMessage and output Greeting are just POJOs representing the body of the messages being sent and returned.

public class Greeting {
    private String content;
    public Greeting(String content) {
           this.content = content;
    }
    public String getContent() {
      return content;
    }
}
public class HelloMessage {
    private String name;
    public String getName() {
        return name;
    }
}

Client sockJS and STOMP codes

On the client side, I use the sockJS protocol fallback option as outlined in the Spring documentation. The javascript codes are included below

// Create stomp client over sockJS protocol (see Note 1)
 var socket = new SockJS("/hello");
 var stompClient = Stomp.over(socket);

 // callback function to be called when stomp client is connected to server (see Note 2)
 var connectCallback = function() {
      alert("connected!");
      stompClient.subscribe('/topic/greetings', function(greeting){
           alert(JSON.parse(greeting.body).content);
      });
 }; 

 // callback function to be called when stomp client could not connect to server (see Note 3)
 var errorCallback = function(error) {
      // display the error's message header:
      alert(error.headers.message);
 };

 // Connect as guest (Note 4)
 stompClient.connect("guest", "guest", connectCallback, errorCallback);

Note

  1. The client starts by create a sockJS by specifying the endpoint (ie. /hello) to connect to and then a stomp client is created over the socket. The endpoint here should match that defined in the Spring MVC configuration in the lines. Note also the 2nd line referring to sockJS.

    <websocket:stomp-endpoint path=”/hello”>
    <websocket:sockjs/>
    </websocket:stomp-endpoint>

  2. Then a callback function is created and assigned to a variable connectCallback. This is called when a successful connection is made by the stomp client. This allows us to start making subscriptions to messages (as in codes, repeated below) and sending messages. Note the subscription is for the topic “/topic/greetings”

    stompClient.subscribe(‘/topic/greetings’, function(greeting){
    alert(JSON.parse(greeting.body).content);
    });

  3. A error callback function is defined if stomp client fails to connect to server.
  4. This line makes the connection registering the callback functions.

Now we are ready to send messages from the client, e.g. using the following javascript function

// function to send message
 function fnSayHi() {
       stompClient.send("/app/hello", {}, JSON.stringify({ 'name': 'Joe' }));
 }

The message will be sent to the Spring MVC message handler method greeting() as defined via the annotation @MessageMapping(“/hello”).

 <websocket:message-broker application-destination-prefix=”/app”>

Note the prefix “/app” is defined in the Spring config as  application-destination-prefix attribute of the message broker: Note also, the use of @SendTo annotation to direct the message to a given destination. I repeat the controller method below

 @MessageMapping("/hello")
 @SendTo("/topic/greetings")
 public Greeting greeting(HelloMessage message) throws Exception {
      return new Greeting("Hello, " + message.getName() + "!");
 }

That’s it for now.

Using JBoss Drools to Implement a E-commerce Promotion Rule Engine – Part 2

In my last post, I outline several common E-commerce promotion rules to be implemented with JBoss Drools Expert. I also describe the domain objects for representing the facts used by the rule engine as well as set up the unit test harness. I will finish off with this post by providing details on how each rule can be defined with the purpose of demonstrating some capabilities of Drools.

Drools Rules

Rule 1: Large order discount

Rule file

// 1. Apply discount if order is over a certain amount
 rule "$50 discount for order over $1000 but less than $2000"
 no-loop
 when
      $order : Order(
           $order.lineTotal >= 1000 &&
           $order.lineTotal < 2000
      )
 then
      modify($order) {
           setOrderDiscountAmount(50);
      }
      System.out.println("Apply $50 discount for order over $1000:");
      System.out.println($order);
 end
rule "200 discount for order over $2000"
 no-loop
 when
      $order : Order(
           $order.lineTotal >= 2000
      )
 then
      modify($order) {
           setOrderDiscountAmount(200);
      }
      System.out.println("Apply $200 discount for order over $2000:");
      System.out.println($order);
 end

Note

  1. 2 rules are defined here. The first one applies when the order total (lineTotal) is between $1000 and $2000. The second rule applies for any order over $2000.
  2. The modify keyword in the action (then) clause tells the rule engine that the rule modifies the fact, in this case the order. This would have triggered the same  rule again. Drools use the rule attribute no-loop to stop this from happening

Unit Test

@Test
 public void testLargeOrderDiscount_over1000() {
      // Add facts to working memory
      Order order = new Order();
      order.addOrderLine(new OrderLine("SKU 123", 1, 1200));
      ksession.insert(order);
      assertEquals(1, ksession.fireAllRules());
 }
Output:
Apply $50 discount for order over $1000:
 Order total(discount): 1200.0(50.0)
 [SKU 123] x1: 1200.0(-0.0) = 1200.0

Rule 2: Clearance products – 10% of list of defined products

Rule File

// 2. Clearance products
 rule "Clearance products"
 no-loop
 when
      $order : Order()
      $item : OrderLine() from $order.lines
      ClearanceProductList(skus contains $item.getSku())
 then
      System.out.println("Apply discount for clearance product " + $item.getSku() + ":");
      $item.setLineDiscountAmount(10);
      System.out.println($order);
 end

Note

  1. Note the use of keyword from in the when clause to iterate list of order items from the order
  2. Each item found in (1) is set in variable $item, which is then used to check in the next line ClearanceProductList(…) whether the line item is one of the discounted product list skus by using the contains keyword.
  3. Discount is applied to the item in the then clause  $item.setLineDiscountAmount(10);

Unit Test

@Test
 public void testClearanceProductDiscount() {
     // Add facts to working memory
     Order order = new Order();
     order.addOrderLine(new OrderLine("SKU 123", 1, 100));
     order.addOrderLine(new OrderLine("SKU 456", 1, 200));
     ClearanceProductList productList = new ClearanceProductList();
     productList.add("SKU 123");
     productList.add("SKU ABC");
     ksession.insert(order);
     ksession.insert(productList);
     assertEquals(2, ksession.fireAllRules());
}
Output:
Apply discount for clearance product SKU 123:
 Order total(discount): 290.0(0.0)
 [SKU 123] x1: 100.0(-10.0) = 90.0
 [SKU 456] x1: 200.0(-0.0) = 200.0

Rule 3: Time based sales – 10% off from a list of defined products within a certain date range, e.g. Christmas sales between 1/12 to 31/12.

Rule File

// 3. Time based sales
rule "Time based sales"
no-loop
 when
      $order : Order()
      $item : OrderLine() from $order.lines
      $sales : TimeBasedSales(
           fromDate < LocalDate.now().toDate(),
           toDate > LocalDate.now().toDate(),
           products contains $item.getSku()
     )
 then
      $item.setLineDiscountAmount($item.getUnitPrice() * $item.getQty() * 0.1);
      System.out.println("Apply time based discount for product " + $item.getSku() + ":");
      System.out.println($order);
end

Note:

  1. The date comparisons for fromDate and toDate in the last lines of the when clause.

Unit Test

@Test
 public void testTimeBasedSales() {
      // Add facts to working memory
      Order order = new Order();
      order.addOrderLine(new OrderLine("SKU 123", 1, 100));
      order.addOrderLine(new OrderLine("SKU 456", 1, 200));
      TimeBasedSales sales = new TimeBasedSales(
      LocalDate.now().minusDays(5).toDate(),
      LocalDate.now().plusDays(5).toDate());
      sales.addProduct("SKU 456");

      ksession.insert(order);
      ksession.insert(sales); 
      assertEquals(2, ksession.fireAllRules()); 
 }
Output:
Apply time based discount for product SKU 456:
Order total(discount): 280.0(0.0)
[SKU 123] x1: 100.0(-0.0) = 100.0
[SKU 456] x1: 200.0(-20.0) = 180.0

Note:

  1. I setup the TimeBasedSales object based on current date. Drools supports concept of pseudo system clock which allows you to test time related rules independent of current real time. See here for details.

Rule 4: Special Tuesday – everything 5% off on Tuesday

Rule File

// 4. 5% discount everything Tuesday
rule "5% discount everything tuesday"
calendars "tuesdays"
 when
      $order : Order()
 then
      System.out.println("Apply special Tuesday discount:");
      $order.setOrderDiscountAmount(0.05 * $order.getLineTotal());
System.out.println($order);
end

Note

  1. This rule makes use of attribute calendars to define the pre-condition that the rule will be considered. The value “tuesdays” correspond to the calendar of same name defined in the knowledge base session. Below is the code in the unit test. Note the use of Quartz to define days of week excluded.
private void getTuesdayCalendar() {
      //in this case I'm using a DailyCalendar but you can use whatever implementation of Calendar you want
       org.quartz.impl.calendar.WeeklyCalendar tuesdays = new org.quartz.impl.calendar.WeeklyCalendar();
       tuesdays.setDaysExcluded(new boolean[] {true, true, true, false, true, true, true, true}); // index 1 = Sunday, etc.
      //convert the calendar into a org.drools.time.Calendar
      org.drools.time.Calendar tuesdayCalendar = QuartzHelper.quartzCalendarAdapter(tuesdays);

      //Register the calendar in the session with a name. You must use this name in your rules.
      ksession.getCalendars().set( "tuesdays", tuesdayCalendar);
}

Unit Test

@Test
 public void testSpecialTuesdaysApplied() {
      // Move to next Tuesday
      advanceToDayOfWeek(2);
      // Add facts to working memory
      Order order = new Order();
      order.addOrderLine(new OrderLine("SKU 123", 1, 100));
      order.addOrderLine(new OrderLine("SKU 456", 1, 200));
      ksession.insert(order);
      assertEquals(2, ksession.fireAllRules());
 }

Output:
Apply special Tuesday discount:
Order total(discount): 300.0(15.0)
[SKU 123] x1: 100.0(-0.0) = 100.0
[SKU 456] x1: 200.0(-0.0) = 200.0

Note

  1. I use the psuedo system clock and method advanceToDayOfWeek(2) to move the clock to next Tuesday to trigger the rule.

Rules Interactions

In a real world commerce environment, a promotion engine will have a considerably bigger number of business rules. Importantly, those rules will be interdependent to each other. Typically, only one rule out of a number of different rules can apply. Alternatively, there is a specific order in which certain rules should apply. Rules can potentially be conflicting. This is where a rule engine like Drools comes in handy as it provides a comprehensive set of features to support these and other common scenarios.

salience

This rule attribute is used to define the priority in which a rule is fired in relation to other rules. For example, a large value discount (Rule 1) should only be applied after all the other line item discounts are applied.

activation-group

This rule attribute is used to group a number of related rules so that when one rule in the group fires, the other rules become inactive. For example, if a product is in clearance category, only the clearance discount is applied.

Final Thoughts

I hope this and last posts demonstrate how common ecommerce problem such as promotion can be solved using a business rule engine such as Drools. Obviously, the business rules will be more complex in the real world but this article should illustrate by example the benefits I listed in Part 1, which I explain in more details below:

  1. Declarative Programming – business rules are defined declaratively in pseudo human readable format (when … then) which promotes common understanding between business and developers.
  2. Separation of Data and Logic – all the logics are defined as rules. This is a significant advantage as it scales much better than having to implement the logics in codes. Also, business rules change often in real world and it is advantageous here as the rules can be modified without need of code changes.
  3. Centralization of Knowledge – the set of rules defined declaratively become the knowledge base of the system (and the business), which will faciliate its update and maintainence.
  4. Explanation of outcomes or actions – the rule engine can answer the why-questions as it knows what rules are fired based on what facts. This is considerably more difficult to implement without the Inference Engine implemented by the rule engine.

There are other ecommerce problems that could benefit from Drools. For example, the determination of product pricing based on order, customer, product costs, etc.

As for Drools, I only touch on the basic features. JBoss has good documentation which can be found here. A few features worth exploring include

  1. Decision table – instead of defining rules in drl files as shown here, drools also support writing rules in MS-Excel spreadsheets
  2. Rule template – instead of defining individual rules, one can define a template with variables and then create the individual rules by filling in the values of the variables from data in database or MS-Excel files.
  3. Event processing in the API

That’s all for now.