Sunday, January 11, 2015

JAX-WS - CXF logging request and response SOAP messages using Log4j

apache cxf logo
CXF uses Java SE Logging for both client- and server-side logging of SOAP requests and responses. Logging is activated by use of separate in/out interceptors that can be attached to the requester and/or provider as required. These interceptors can be specified either programmatically (via Java code and/or annotations) or via use of configuration files. The following code sample shows how to configure CXF interceptors using Log4j for the Hello World web service from a previous post.

Tools used:
  • CXF 3.0
  • Spring 4.1
  • Log4j 1.2
  • Jetty 8
  • Maven 3

Specifying the interceptors via configuration files offers two benefits over programmatic configuration:
  1. Logging requirements can be altered without needing to recompile the code
  2. No Apache CXF-specific APIs need to be added to your code, which helps it remain interoperable with other JAX-WS compliant web service stacks

For this example Log4j will be used which is is a Java-based logging utility. As a best practice the CXF java.util.logging calls will first be redirected to SLF4J (Simple Logging Facade for Java) as described here. In other words a 'META-INF/cxf/org.apache.cxf.Logger' file will be created on the classpath containing the following:
org.apache.cxf.common.logging.Slf4jLogger

As the Hello World example uses Spring, the commons-logging calls from the Spring framework will also be redirected to SLF4J using jcl-over-slf4j. Now that all logging calls of both CXF and Spring are redirected to SLF4J, Log4j will be plugged into SLF4J using the slf4j-log4j12 adaptation layer. The picture below illustrates the described approach.
cxf logging using log4j

The below Maven POM file contains the needed dependencies for the SLF4 bridge (jcl-over-slf4j), Log4j adaptation layer (slf4j-log4j12) and Log4j (log4j). In addition it contains all other needed dependencies and plugins needed in order to run the example.

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<groupId>info.source4code</groupId>
<artifactId>jaxws-jetty-cxf-logging-log4j</artifactId>
<version>1.0</version>
<packaging>war</packaging>

<name>JAX-WS - CXF logging request and response SOAP messages using Log4j</name>
<url>http://www.source4code.info/2015/01/jaxws-cxf-logging-request-response-soap-messages-log4j.html</url>

<prerequisites>
<maven>3.0</maven>
</prerequisites>

<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<java.version>1.6</java.version>

<slf4j.version>1.7.10</slf4j.version>
<log4j.version>1.2.17</log4j.version>
<junit.version>4.12</junit.version>
<cxf.version>3.0.3</cxf.version>
<spring.version>4.1.4.RELEASE</spring.version>

<maven-compiler-plugin.version>3.1</maven-compiler-plugin.version>
<jetty-maven-plugin.version>8.1.14.v20131031</jetty-maven-plugin.version>
<maven-failsafe-plugin.version>2.6</maven-failsafe-plugin.version>
</properties>

<dependencies>
<!-- Logging -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>jcl-over-slf4j</artifactId>
<version>${slf4j.version}</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>${slf4j.version}</version>
</dependency>
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>${log4j.version}</version>
</dependency>
<!-- JUnit -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>${junit.version}</version>
<scope>test</scope>
</dependency>
<!-- CXF -->
<dependency>
<groupId>org.apache.cxf</groupId>
<artifactId>cxf-rt-frontend-jaxws</artifactId>
<version>${cxf.version}</version>
</dependency>
<dependency>
<groupId>org.apache.cxf</groupId>
<artifactId>cxf-rt-transports-http</artifactId>
<version>${cxf.version}</version>
</dependency>
<dependency>
<groupId>org.apache.cxf</groupId>
<!-- Jetty is needed if you're are not using the CXFServlet -->
<artifactId>cxf-rt-transports-http-jetty</artifactId>
<version>${cxf.version}</version>
</dependency>
<!-- Spring -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>${spring.version}</version>
<exclusions>
<exclusion>
<artifactId>commons-logging</artifactId>
<groupId>commons-logging</groupId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
<version>${spring.version}</version>
<exclusions>
<exclusion>
<artifactId>commons-logging</artifactId>
<groupId>commons-logging</groupId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>${spring.version}</version>
<scope>test</scope>
<exclusions>
<exclusion>
<artifactId>commons-logging</artifactId>
<groupId>commons-logging</groupId>
</exclusion>
</exclusions>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>${maven-compiler-plugin.version}</version>
<configuration>
<source>${java.version}</source>
<target>${java.version}</target>
</configuration>
</plugin>
<!-- jetty-maven-plugin -->
<plugin>
<groupId>org.mortbay.jetty</groupId>
<artifactId>jetty-maven-plugin</artifactId>
<version>${jetty-maven-plugin.version}</version>
<configuration>
<connectors>
<connector
implementation="org.eclipse.jetty.server.nio.SelectChannelConnector">
<port>9090</port>
</connector>
</connectors>
<stopPort>8005</stopPort>
<stopKey>STOP</stopKey>
<daemon>true</daemon>
</configuration>
<executions>
<execution>
<id>start-jetty</id>
<phase>pre-integration-test</phase>
<goals>
<goal>start</goal>
</goals>
</execution>
<execution>
<id>stop-jetty</id>
<phase>post-integration-test</phase>
<goals>
<goal>stop</goal>
</goals>
</execution>
</executions>
</plugin>
<!-- maven-failsafe-plugin -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-failsafe-plugin</artifactId>
<version>${maven-failsafe-plugin.version}</version>
<executions>
<execution>
<goals>
<goal>integration-test</goal>
<goal>verify</goal>
</goals>
</execution>
</executions>
</plugin>
<!-- cxf-codegen-plugin -->
<plugin>
<groupId>org.apache.cxf</groupId>
<artifactId>cxf-codegen-plugin</artifactId>
<version>${cxf.version}</version>
<executions>
<execution>
<id>generate-sources</id>
<phase>generate-sources</phase>
<configuration>
<sourceRoot>${project.build.directory}/generated/cxf</sourceRoot>
<wsdlOptions>
<wsdlOption>
<wsdl>${project.basedir}/src/main/resources/wsdl/helloworld.wsdl</wsdl>
<wsdlLocation>classpath:wsdl/helloworld.wsdl</wsdlLocation>
</wsdlOption>
</wsdlOptions>
</configuration>
<goals>
<goal>wsdl2java</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>

Log4j configuration

The log4j environment is fully configurable programmatically. However, it is far more flexible to configure log4j using configuration files. Currently, configuration files can be written in XML or in Java properties (key=value) format. For this example a configuration script in XML is used as shown below. The script will write all logging events to a file except for the request and response SOAP messages that will be written to a different file.

Two appenders are configured to write logging events to a rolling file. The first file 'jaxws-jetty-cxf-logging.log' will contain all log events except for the ones emitted by the CXF logging interceptors as these are going to be written to 'jaxws-jetty-cxf-logging-ws.log'.

The last section contains the different loggers and the level at which information is logged. The log level of the 'org.apache.cxf.services' logger needs to be set to INFO in order to activate the SOAP logging events. In addition the WS_LOG_FILE appender needs to be referenced in order to write the SOAP logging events to a different file. Note the additivity="false" which makes sure the log events are not written to appenders attached to its ancestors (in this case APP_LOG_FILE).

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE log4j:configuration SYSTEM "log4j.dtd">
<log4j:configuration debug="false"
xmlns:log4j='http://jakarta.apache.org/log4j/'>

<!-- APPENDERS -->
<appender name="APP_LOG_FILE" class="org.apache.log4j.DailyRollingFileAppender">
<param name="file" value="jaxws-jetty-cxf-logging.log" />
<param name="DatePattern" value="'.'yyyy-MM-dd" />
<layout class="org.apache.log4j.PatternLayout">
<param name="ConversionPattern" value="%d{HH:mm:ss.SSS} %-5p [%t][%c{1}] %m%n" />
</layout>
</appender>

<appender name="WS_LOG_FILE" class="org.apache.log4j.DailyRollingFileAppender">
<param name="file" value="jaxws-jetty-cxf-logging-ws.log" />
<param name="DatePattern" value="'.'yyyy-MM-dd" />
<layout class="org.apache.log4j.PatternLayout">
<param name="ConversionPattern" value="%d{HH:mm:ss.SSS} %-5p [%t][%c{1}] %m%n" />
</layout>
</appender>

<!-- LOGGERS -->
<logger name="info.source4code.soap.http.cxf">
<level value="INFO" />
</logger>

<logger name="org.springframework">
<level value="WARN" />
</logger>
<logger name="org.apache.cxf">
<level value="WARN" />
</logger>

<!-- level INFO needed to log SOAP messages -->
<logger name="org.apache.cxf.services" additivity="false">
<level value="INFO" />
<!-- specify a dedicated appender for the SOAP messages -->
<appender-ref ref="WS_LOG_FILE" />
</logger>

<root>
<level value="WARN" />
<appender-ref ref="APP_LOG_FILE" />
</root>

</log4j:configuration>

Requester

In order to activate the logging interceptors provided by the CXF framework, there are two options. For the requester(client) the option where all logging interceptors are configured manually will be illustrated. The other option, where the logging feature is used to configure all interceptors, will be shown in the provider section down below. For more information on the difference between interceptors and features check out this post.

First an instance of the abstract AbstractLoggingInterceptor class is created to enable pretty printing of the SOAP messages. Next, instances of the LoggingInInterceptor and LoggingOutInterceptor are specified which have as parent the previously defined abstractLoggingInterceptor instance. In a last step the interceptors are added to the CXF bus, which is the backbone of the CXF architecture that manages the respective inbound and outbound message and fault interceptor chains for all client and server endpoints. The interceptors are added to both in/out and respective fault interceptor chains as shown below.
Note that interceptors can be added to a client, server or bus.
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:cxf="http://cxf.apache.org/core"
xmlns:jaxws="http://cxf.apache.org/jaxws"
xsi:schemaLocation="
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://cxf.apache.org/core http://cxf.apache.org/schemas/core.xsd
http://cxf.apache.org/jaxws http://cxf.apache.org/schemas/jaxws.xsd">

<jaxws:client id="helloWorldRequesterBean"
serviceClass="info.source4code.services.helloworld.HelloWorldPortType"
address="${helloworld.address}">
</jaxws:client>

<!-- abstractLoggingInterceptor that will enable pretty printing of messages -->
<bean id="abstractLoggingInterceptor" abstract="true">
<property name="prettyLogging" value="true" />
</bean>

<!-- logging interceptors that will log all received/sent messages -->
<bean id="loggingInInterceptor" class="org.apache.cxf.interceptor.LoggingInInterceptor"
parent="abstractLoggingInterceptor">
</bean>
<bean id="loggingOutInterceptor" class="org.apache.cxf.interceptor.LoggingOutInterceptor"
parent="abstractLoggingInterceptor">
</bean>

<!-- add the logging interceptors to the CXF bus -->
<cxf:bus>
<cxf:inInterceptors>
<ref bean="loggingInInterceptor" />
</cxf:inInterceptors>
<cxf:inFaultInterceptors>
<ref bean="loggingInInterceptor" />
</cxf:inFaultInterceptors>
<cxf:outInterceptors>
<ref bean="loggingOutInterceptor" />
</cxf:outInterceptors>
<cxf:outFaultInterceptors>
<ref bean="loggingOutInterceptor" />
</cxf:outFaultInterceptors>
</cxf:bus>

</beans>

Provider

Activating the interceptors at provider(server) side will be done using the LoggingFeature that is supplied with the CXF framework.

First an instance of the abstract LoggingFeature class is created with the prettyLogging set to true in order to enable pretty printing of the SOAP messages. As with the interceptors, the feature is added to the CXF bus in order to activate them as shown below.
Note that features can be added to a client, server or a bus.
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:cxf="http://cxf.apache.org/core"
xmlns:jaxws="http://cxf.apache.org/jaxws"
xsi:schemaLocation="
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://cxf.apache.org/core http://cxf.apache.org/schemas/core.xsd
http://cxf.apache.org/jaxws http://cxf.apache.org/schemas/jaxws.xsd">

<jaxws:endpoint id="helloWorldProviderBean"
implementor="info.source4code.soap.http.cxf.HelloWorldImpl"
address="/helloworld">
</jaxws:endpoint>

<!-- loggingFeature that will log all received/sent messages -->
<bean id="loggingFeature" class="org.apache.cxf.feature.LoggingFeature">
<property name="prettyLogging" value="true" />
</bean>

<!-- add the loggingFeature to the cxf bus -->
<cxf:bus>
<cxf:features>
<ref bean="loggingFeature" />
</cxf:features>
</cxf:bus>

</beans>

Testing

Testing of the service is done using a unit and an integration test case. For the unit test case the provider is created without Spring (using JaxWsServerFactoryBean), as such the logging feature needs to be added programmatically as shown below.
package info.source4code.soap.http.cxf;

import static org.junit.Assert.assertEquals;
import info.source4code.services.helloworld.Person;

import org.apache.cxf.feature.LoggingFeature;
import org.apache.cxf.jaxws.JaxWsServerFactoryBean;
import org.junit.BeforeClass;
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;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = { "classpath:/META-INF/spring/context-requester.xml" })
public class HelloWorldImplTest {

private static String ENDPOINT_ADDRESS = "http://localhost:9090/s4c/services/helloworld";

@Autowired
private HelloWorldClient helloWorldClient;

@BeforeClass
public static void setUpBeforeClass() throws Exception {
// start the HelloWorld service using jaxWsServerFactoryBean
JaxWsServerFactoryBean jaxWsServerFactoryBean = new JaxWsServerFactoryBean();

// adding loggingFeature to print the received/sent messages
LoggingFeature loggingFeature = new LoggingFeature();
loggingFeature.setPrettyLogging(true);

jaxWsServerFactoryBean.getFeatures().add(loggingFeature);

// setting the implementation
HelloWorldImpl implementor = new HelloWorldImpl();
jaxWsServerFactoryBean.setServiceBean(implementor);
// setting the endpoint
jaxWsServerFactoryBean.setAddress(ENDPOINT_ADDRESS);
jaxWsServerFactoryBean.create();
}

@Test
public void testSayHello() {
Person person = new Person();
person.setFirstName("John");
person.setLastName("Doe");

assertEquals("Hello John Doe!",
helloWorldClient.sayHello(person));
}
}

Run the example by opening a command prompt and executing following Maven command:
mvn verify

The result should be that a number of log files are created in the project root directory. Amongst these files there should be 'jaxws-jetty-cxf-logging-ws.log' and 'jaxws-jetty-cxf-logging-ws-test.log' which contain the exchanged SOAP messages.
06:26:41.718 INFO  [qtp1766911175-27][HelloWorld_PortType] Inbound Message
----------------------------
ID: 1
Address: http://localhost:9090/s4c/services/helloworld
Encoding: UTF-8
Http-Method: POST
Content-Type: text/xml; charset=UTF-8
Headers: {Accept=[*/*], Cache-Control=[no-cache], connection=[keep-alive], Content-Length=[229], content-type=[text/xml; charset=UTF-8], Host=[localhost:9090], Pragma=[no-cache], SOAPAction=[""], User-Agent=[Apache CXF 3.0.3]}
Payload: <soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
<soap:Body>
<person xmlns="http://source4code.info/services/helloworld">
<firstName>John</firstName>
<lastName>Doe</lastName>
</person>
</soap:Body>
</soap:Envelope>

--------------------------------------
06:26:41.786 INFO [qtp1766911175-27][HelloWorld_PortType] Outbound Message
---------------------------
ID: 1
Response-Code: 200
Encoding: UTF-8
Content-Type: text/xml
Headers: {}
Payload: <soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
<soap:Body>
<greeting xmlns="http://source4code.info/services/helloworld">
<text>Hello John Doe!</text>
</greeting>
</soap:Body>
</soap:Envelope>


github icon
If you would like to run the above code sample you can download the full source code and their corresponding JUnit and integration test cases here.

This concludes the CXF logging request and response messages using Log4j example. If you found this post helpful or have any questions or remarks, please leave a comment.

Saturday, January 10, 2015

JAX-WS - CXF logging request and response SOAP messages using Log4j2

apache cxf logo
CXF uses Java SE Logging for both client- and server-side logging of SOAP requests and responses. Logging is activated by use of separate in/out interceptors that can be attached to the requester and/or provider as required. These interceptors can be specified either programmatically (via Java code and/or annotations) or via use of configuration files. The following code sample shows how to configure CXF interceptors using Log4j2 for the Hello World web service from a previous post.

Tools used:
  • CXF 3.0
  • Spring 4.1
  • Log4j2 2.1
  • Jetty 8
  • Maven 3

Specifying the interceptors via configuration files offers two benefits over programmatic configuration:
  1. Logging requirements can be altered without needing to recompile the code
  2. No Apache CXF-specific APIs need to be added to your code, which helps it remain interoperable with other JAX-WS compliant web service stacks

For this example Log4j2 will be used which is an upgrade of the Log4j project. As a best practice the CXF java.util.logging calls will first be redirected to SLF4J (Simple Logging Facade for Java) as described here. In other words a 'META-INF/cxf/org.apache.cxf.Logger' file will be created on the classpath containing the following:
org.apache.cxf.common.logging.Slf4jLogger

As the Hello World example uses Spring, the commons-logging calls from the Spring framework will also be redirected to SLF4J using jcl-over-slf4j. Now that all logging calls of both CXF and Spring are redirected to SLF4J, Log4j2 will be plugged into SLF4J using the Log4j 2 SLF4J Binding. The picture below illustrates the described approach.
cxf logging using log4j2

The below Maven POM file contains the needed dependencies for the SLF4 bridge (jcl-over-slf4j), Log4j 2 SLF4J Binding (log4j-slf4j-impl) and Log4j2 (log4j-core). In addition it contains all other needed dependencies and plugins needed in order to run the example.
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<groupId>info.source4code</groupId>
<artifactId>jaxws-jetty-cxf-logging-log4j2</artifactId>
<version>1.0</version>
<packaging>war</packaging>

<name>JAX-WS - CXF logging request and response SOAP messages using Log4j2</name>
<url>http://www.source4code.info/2015/01/jaxws-cxf-logging-request-response-soap-messages-log4j2.html</url>

<prerequisites>
<maven>3.0</maven>
</prerequisites>

<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<java.version>1.6</java.version>

<slf4j.version>1.7.10</slf4j.version>
<log4j2.version>2.1</log4j2.version>
<junit.version>4.12</junit.version>
<cxf.version>3.0.3</cxf.version>
<spring.version>4.1.4.RELEASE</spring.version>

<maven-compiler-plugin.version>3.1</maven-compiler-plugin.version>
<jetty-maven-plugin.version>8.1.16.v20140903</jetty-maven-plugin.version>
<maven-failsafe-plugin.version>2.17</maven-failsafe-plugin.version>
</properties>

<dependencies>
<!-- Logging -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>jcl-over-slf4j</artifactId>
<version>${slf4j.version}</version>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-slf4j-impl</artifactId>
<version>${log4j2.version}</version>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
<version>${log4j2.version}</version>
</dependency>
<!-- JUnit -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>${junit.version}</version>
<scope>test</scope>
</dependency>
<!-- CXF -->
<dependency>
<groupId>org.apache.cxf</groupId>
<artifactId>cxf-rt-frontend-jaxws</artifactId>
<version>${cxf.version}</version>
</dependency>
<dependency>
<groupId>org.apache.cxf</groupId>
<artifactId>cxf-rt-transports-http</artifactId>
<version>${cxf.version}</version>
</dependency>
<dependency>
<groupId>org.apache.cxf</groupId>
<!-- Jetty is needed if you're are not using the CXFServlet -->
<artifactId>cxf-rt-transports-http-jetty</artifactId>
<version>${cxf.version}</version>
</dependency>
<!-- Spring -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>${spring.version}</version>
<exclusions>
<exclusion>
<artifactId>commons-logging</artifactId>
<groupId>commons-logging</groupId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
<version>${spring.version}</version>
<exclusions>
<exclusion>
<artifactId>commons-logging</artifactId>
<groupId>commons-logging</groupId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>${spring.version}</version>
<scope>test</scope>
<exclusions>
<exclusion>
<artifactId>commons-logging</artifactId>
<groupId>commons-logging</groupId>
</exclusion>
</exclusions>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>${maven-compiler-plugin.version}</version>
<configuration>
<source>${java.version}</source>
<target>${java.version}</target>
</configuration>
</plugin>
<!-- jetty-maven-plugin -->
<plugin>
<groupId>org.mortbay.jetty</groupId>
<artifactId>jetty-maven-plugin</artifactId>
<version>${jetty-maven-plugin.version}</version>
<configuration>
<connectors>
<connector
implementation="org.eclipse.jetty.server.nio.SelectChannelConnector">
<port>9090</port>
</connector>
</connectors>
<stopPort>8005</stopPort>
<stopKey>STOP</stopKey>
<daemon>true</daemon>
</configuration>
<executions>
<execution>
<id>start-jetty</id>
<phase>pre-integration-test</phase>
<goals>
<goal>start</goal>
</goals>
</execution>
<execution>
<id>stop-jetty</id>
<phase>post-integration-test</phase>
<goals>
<goal>stop</goal>
</goals>
</execution>
</executions>
</plugin>
<!-- maven-failsafe-plugin -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-failsafe-plugin</artifactId>
<version>${maven-failsafe-plugin.version}</version>
<executions>
<execution>
<goals>
<goal>integration-test</goal>
<goal>verify</goal>
</goals>
</execution>
</executions>
</plugin>
<!-- cxf-codegen-plugin -->
<plugin>
<groupId>org.apache.cxf</groupId>
<artifactId>cxf-codegen-plugin</artifactId>
<version>${cxf.version}</version>
<executions>
<execution>
<id>generate-sources</id>
<phase>generate-sources</phase>
<configuration>
<sourceRoot>${project.build.directory}/generated/cxf</sourceRoot>
<wsdlOptions>
<wsdlOption>
<wsdl>${project.basedir}/src/main/resources/wsdl/helloworld.wsdl</wsdl>
<wsdlLocation>classpath:wsdl/helloworld.wsdl</wsdlLocation>
</wsdlOption>
</wsdlOptions>
</configuration>
<goals>
<goal>wsdl2java</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>

Log4j2 configuration

Log4j2 can be configured in 1 of 4 ways:
  • Through a configuration file written in XML, JSON, or YAML.
  • Programmatically, by creating a ConfigurationFactory and Configuration implementation.
  • Programmatically, by calling the APIs exposed in the Configuration interface to add components to the default configuration.
  • Programmatically, by calling methods on the internal Logger class.

For this example a configuration script in XML is used as shown below. The script will write all logging events to a file except for the request and response SOAP messages that will be written to a different file.

After defining some properties that contain the layout pattern and file names, two appenders are configured to write logging events to a rolling file. The first file 'jaxws-jetty-cxf-logging.log' will contain all log events except for the ones emitted by the CXF logging interceptors as these are going to be written to 'jaxws-jetty-cxf-logging-ws.log'.

The last section contains the different loggers and the level at which information is logged. The log level of the 'org.apache.cxf.services' logger needs to be set to INFO in order to activate the SOAP logging events. In addition the WS_LOG_FILE appender needs to be referenced in order to write the SOAP logging events to a different file. Note the additivity="false" which makes sure the log events are not written to appenders attached to its ancestors (in this case APP_LOG_FILE).
<?xml version="1.0" encoding="UTF-8"?>
<Configuration monitorInterval="60">

<!-- PROPERTIES -->
<Properties>
<Property name="layout">%d{HH:mm:ss.SSS} %-5level [%thread][%logger{0}] %m%n</Property>
<Property name="logFile">jaxws-jetty-cxf-logging</Property>
<Property name="logFile-ws">jaxws-jetty-cxf-logging-ws</Property>
</Properties>

<!-- APPENDERS -->
<Appenders>
<RollingFile name="APP_LOG_FILE" fileName="${logFile}.log" filePattern="${logFile}.%d{yyyy-MM-dd}.log">
<TimeBasedTriggeringPolicy interval="1" modulate="true" />
<DefaultRolloverStrategy max="30" />
<PatternLayout pattern="${layout}" />
</RollingFile>

<RollingFile name="WS_LOG_FILE" fileName="${logFile-ws}.log" filePattern="${logFile-ws}.%d{yyyy-MM-dd}.log">
<TimeBasedTriggeringPolicy interval="1" modulate="true" />
<DefaultRolloverStrategy max="30" />
<PatternLayout pattern="${layout}" />
</RollingFile>
</Appenders>

<!-- LOGGERS -->
<Loggers>
<Logger name="info.source4code.soap.http.cxf" level="INFO" />

<Logger name="org.springframework" level="WARN" />
<Logger name="org.apache.cxf" level="WARN" />

<!-- level INFO needed to log SOAP messages -->
<Logger name="org.apache.cxf.services" level="INFO"
additivity="false">
<!-- specify a dedicated appender for the SOAP messages -->
<AppenderRef ref="WS_LOG_FILE" />
</Logger>

<!-- APPLICATION LOG LEVEL -->
<Root level="WARN">
<AppenderRef ref="APP_LOG_FILE" />
</Root>
</Loggers>

</Configuration>

Requester

In order to activate the logging interceptors provided by the CXF framework, there are two options. For the requester(client) the option where all logging interceptors are configured manually will be illustrated. The other option, where the logging feature is used to configure all interceptors, will be shown in the provider section down below. For more information on the difference between interceptors and features check out this post.

First an instance of the abstract AbstractLoggingInterceptor class is created to enable pretty printing of the SOAP messages. Next, instances of the LoggingInInterceptor and LoggingOutInterceptor are specified which have as parent the previously defined abstractLoggingInterceptor instance. In a last step the interceptors are added to the CXF bus, which is the backbone of the CXF architecture that manages the respective inbound and outbound message and fault interceptor chains for all client and server endpoints. The interceptors are added to both in/out and respective fault interceptor chains as shown below.
Note that interceptors can be added to a client, server or bus.
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:cxf="http://cxf.apache.org/core"
xmlns:jaxws="http://cxf.apache.org/jaxws"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://cxf.apache.org/core http://cxf.apache.org/schemas/core.xsd
http://cxf.apache.org/jaxws http://cxf.apache.org/schemas/jaxws.xsd">

<jaxws:client id="helloWorldRequesterBean"
serviceClass="info.source4code.services.helloworld.HelloWorldPortType"
address="${helloworld.address}">
</jaxws:client>

<!-- abstractLoggingInterceptor that will enable pretty printing of messages -->
<bean id="abstractLoggingInterceptor" abstract="true">
<property name="prettyLogging" value="true" />
</bean>

<!-- logging interceptors that will log all received/sent messages -->
<bean id="loggingInInterceptor" class="org.apache.cxf.interceptor.LoggingInInterceptor"
parent="abstractLoggingInterceptor">
</bean>
<bean id="loggingOutInterceptor" class="org.apache.cxf.interceptor.LoggingOutInterceptor"
parent="abstractLoggingInterceptor">
</bean>

<!-- add the logging interceptors to the CXF bus -->
<cxf:bus>
<cxf:inInterceptors>
<ref bean="loggingInInterceptor" />
</cxf:inInterceptors>
<cxf:inFaultInterceptors>
<ref bean="loggingInInterceptor" />
</cxf:inFaultInterceptors>
<cxf:outInterceptors>
<ref bean="loggingOutInterceptor" />
</cxf:outInterceptors>
<cxf:outFaultInterceptors>
<ref bean="loggingOutInterceptor" />
</cxf:outFaultInterceptors>
</cxf:bus>

</beans>

Provider

Activating the interceptors at provider(server) side will be done using the LoggingFeature that is supplied with the CXF framework.

First an instance of the abstract LoggingFeature class is created with the prettyLogging set to true in order to enable pretty printing of the SOAP messages. As with the interceptors, the feature is added to the CXF bus in order to activate them as shown below.
Note that features can be added to a client, server or a bus.
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:cxf="http://cxf.apache.org/core"
xmlns:jaxws="http://cxf.apache.org/jaxws"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://cxf.apache.org/core http://cxf.apache.org/schemas/core.xsd
http://cxf.apache.org/jaxws http://cxf.apache.org/schemas/jaxws.xsd">

<jaxws:endpoint id="helloWorldProviderBean"
implementor="info.source4code.soap.http.cxf.HelloWorldImpl"
address="/helloworld">
</jaxws:endpoint>

<!-- loggingFeature that will log all received/sent messages -->
<bean id="loggingFeature" class="org.apache.cxf.feature.LoggingFeature">
<property name="prettyLogging" value="true" />
</bean>

<!-- add the loggingFeature to the cxf bus -->
<cxf:bus>
<cxf:features>
<ref bean="loggingFeature" />
</cxf:features>
</cxf:bus>

</beans>

Testing

Testing of the service is done using a unit and an integration test case. For the unit test case the provider is created without Spring (using JaxWsServerFactoryBean), as such the logging feature needs to be added programmatically as shown below.
package info.source4code.soap.http.cxf;

import static org.junit.Assert.assertEquals;
import info.source4code.services.helloworld.Person;

import org.apache.cxf.feature.LoggingFeature;
import org.apache.cxf.jaxws.JaxWsServerFactoryBean;
import org.junit.BeforeClass;
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;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = { "classpath:/META-INF/spring/context-requester.xml" })
public class HelloWorldImplTest {

private static String ENDPOINT_ADDRESS = "http://localhost:9090/s4c/services/helloworld";

@Autowired
private HelloWorldClient helloWorldClient;

@BeforeClass
public static void setUpBeforeClass() throws Exception {
// start the HelloWorld service using jaxWsServerFactoryBean
JaxWsServerFactoryBean jaxWsServerFactoryBean = new JaxWsServerFactoryBean();

// adding loggingFeature to print the received/sent messages
LoggingFeature loggingFeature = new LoggingFeature();
loggingFeature.setPrettyLogging(true);

jaxWsServerFactoryBean.getFeatures().add(loggingFeature);

// setting the implementation
HelloWorldImpl implementor = new HelloWorldImpl();
jaxWsServerFactoryBean.setServiceBean(implementor);
// setting the endpoint
jaxWsServerFactoryBean.setAddress(ENDPOINT_ADDRESS);
jaxWsServerFactoryBean.create();
}

@Test
public void testSayHello() {
Person person = new Person();
person.setFirstName("John");
person.setLastName("Doe");

assertEquals("Hello John Doe!",
helloWorldClient.sayHello(person));
}
}

Run the example by opening a command prompt and executing following Maven command:
mvn verify

The result should be that a number of log files are created in the project root directory. Amongst these files there should be 'jaxws-jetty-cxf-logging-ws.log' and 'jaxws-jetty-cxf-logging-ws-test.log' which contain the exchanged SOAP messages.
06:26:41.718 INFO  [qtp1766911175-27][HelloWorld_PortType] Inbound Message
----------------------------
ID: 1
Address: http://localhost:9090/s4c/services/helloworld
Encoding: UTF-8
Http-Method: POST
Content-Type: text/xml; charset=UTF-8
Headers: {Accept=[*/*], Cache-Control=[no-cache], connection=[keep-alive], Content-Length=[229], content-type=[text/xml; charset=UTF-8], Host=[localhost:9090], Pragma=[no-cache], SOAPAction=[""], User-Agent=[Apache CXF 3.0.3]}
Payload: <soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
<soap:Body>
<person xmlns="http://source4code.info/services/helloworld">
<firstName>John</firstName>
<lastName>Doe</lastName>
</person>
</soap:Body>
</soap:Envelope>

--------------------------------------
06:26:41.786 INFO [qtp1766911175-27][HelloWorld_PortType] Outbound Message
---------------------------
ID: 1
Response-Code: 200
Encoding: UTF-8
Content-Type: text/xml
Headers: {}
Payload: <soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
<soap:Body>
<greeting xmlns="http://source4code.info/services/helloworld">
<text>Hello John Doe!</text>
</greeting>
</soap:Body>
</soap:Envelope>


github icon
If you would like to run the above code sample you can download the full source code and their corresponding JUnit and integration test cases here.

This concludes the CXF logging request and response messages using Log4j2 example. If you found this post helpful or have any questions or remarks, please leave a comment.

Wednesday, January 7, 2015

CXF - Feature vs Interceptor

When working with CXF you'll often see that the configuration of a certain capability can be done either via adding a feature or by adding a number of interceptors. In this post we'll briefly explain how the two are related to each other.

CXF Interceptor

Interceptors are the fundamental processing unit inside CXF. For example when a service is invoked, an InterceptorChain (an ordered list of interceptors) is created and invoked. Each interceptor gets a chance to do what they want with the incoming message. This can include reading it, transforming it, processing headers, validating the message, etc. Interceptors can be configured on the interceptor chains of CXF clients, CXF servers or the CXF bus.

When for example configuring logging of all messages on the CXF bus, we need to created the respective LoggingInInterceptor and LoggingOutInterceptor interceptors and add them to the different interceptor chains (InInterceptors, OutInterceptors, OutFaultInterceptors and InFaultInterceptors) which are in turn added to the CXF bus as shown below.
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:cxf="http://cxf.apache.org/core"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://cxf.apache.org/core http://cxf.apache.org/schemas/core.xsd">

<bean id="loggingInInterceptor" class="org.apache.cxf.interceptor.LoggingInInterceptor" />
<bean id="loggingOutInterceptor" class="org.apache.cxf.interceptor.LoggingOutInterceptor" />

<cxf:bus>
<cxf:inInterceptors>
<ref bean="loggingInInterceptor" />
</cxf:inInterceptors>
<cxf:inFaultInterceptors>
<ref bean="loggingInInterceptor" />
</cxf:inFaultInterceptors>
<cxf:outInterceptors>
<ref bean="loggingOutInterceptor" />
</cxf:outInterceptors>
<cxf:outFaultInterceptors>
<ref bean="loggingOutInterceptor" />
</cxf:outFaultInterceptors>
</cxf:bus>

</beans>

CXF Feature

A feature in CXF is a way of adding capabilities to a CXF Client, CXF Server or CXF Bus. In other words features provide a simple way to perform or configure a series of related tasks. These tasks can be things like adding a number of interceptors to a chain, configuring properties, setting up resources, etc.

For example, CXF ships with a LoggingFeature which does exactly the same as the configuration from the previous section. The feature encapsulates the creation of the different logging interceptors and then subsequently adding them to all the interceptor chains.
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:cxf="http://cxf.apache.org/core"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://cxf.apache.org/core http://cxf.apache.org/schemas/core.xsd">

<bean id="loggingFeature" class="org.apache.cxf.feature.LoggingFeature" />

<cxf:bus>
<cxf:features>
<ref bean="loggingFeature" />
</cxf:features>
</cxf:bus>

</beans>

Conclusion

If you need full control when enabling a certain CXF functionality then use interceptors. For example when you only want to log error messages, only configure the logging interceptors on the fault chains.

If you need the standard CXF functionality then use the corresponding feature. For example when you want to log all messages, then configure the logging feature as the notation will be shorter/simpler.

Saturday, January 3, 2015

JSF - PrimeFaces login Servlet Filter example using Jetty and Maven

When creating a Java Server Faces application that needs to ensure only authenticated users can access certain pages, a Servlet Filter in combination with a session managed bean could be used to achieve this. The following post illustrates how to implement a basic PrimeFaces login using Jetty and Maven. It is largely based on this excellent post by BalusC at the stackoverflow forum.


Tools used:
  • JSF 2.2
  • PrimeFaces 5.1
  • Jetty 8
  • Maven 3

The code is built and run using Maven. Specified below is the Maven POM file which contains the needed dependencies for JSF and PrimeFaces. In addition all classes contain corresponding unit test cases which use Mockito and PowerMock for mocking FacesContext as explained in detail in this post. The JSF application will run on a Jetty instance launched from from command line using the jetty-maven-plugin.
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<groupId>info.source4code</groupId>
<artifactId>jsf-jetty-primefaces-login</artifactId>
<version>1.0</version>
<packaging>war</packaging>

<name>JSF - PrimeFaces login using Jetty</name>
<url>http://www.source4code.info/</url>

<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<java.version>1.6</java.version>

<slf4j.version>1.7.7</slf4j.version>
<logback.version>1.1.2</logback.version>
<junit.version>4.11</junit.version>
<mockito.version>1.10.8</mockito.version>
<powermock.version>1.5.6</powermock.version>
<el.version>2.2.1-b04</el.version>
<servlet.version>3.0.1</servlet.version>
<jsf.version>2.2.8-02</jsf.version>
<primefaces.version>5.1</primefaces.version>

<maven-compiler-plugin.version>3.1</maven-compiler-plugin.version>
<jetty-maven-plugin.version>8.1.14.v20131031</jetty-maven-plugin.version>
</properties>

<dependencies>
<!-- Logging -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>${slf4j.version}</version>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>${logback.version}</version>
</dependency>
<!-- JUnit -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>${junit.version}</version>
<scope>test</scope>
</dependency>
<!-- Mockito -->
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>${mockito.version}</version>
<scope>test</scope>
</dependency>
<!-- PowerMock -->
<dependency>
<groupId>org.powermock</groupId>
<artifactId>powermock-core</artifactId>
<version>${powermock.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.powermock</groupId>
<artifactId>powermock-module-junit4</artifactId>
<version>${powermock.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.powermock</groupId>
<artifactId>powermock-api-mockito</artifactId>
<version>${powermock.version}</version>
<scope>test</scope>
</dependency>
<!-- Unified Expression Language (EL) for unit testing -->
<dependency>
<groupId>javax.el</groupId>
<artifactId>el-api</artifactId>
<version>${el.version}</version>
<scope>test</scope>
</dependency>
<!-- Servlet -->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>${servlet.version}</version>
</dependency>
<!-- JSF -->
<dependency>
<groupId>com.sun.faces</groupId>
<artifactId>jsf-api</artifactId>
<version>${jsf.version}</version>
</dependency>
<dependency>
<groupId>com.sun.faces</groupId>
<artifactId>jsf-impl</artifactId>
<version>${jsf.version}</version>
</dependency>
<!-- PrimeFaces -->
<dependency>
<groupId>org.primefaces</groupId>
<artifactId>primefaces</artifactId>
<version>${primefaces.version}</version>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>${maven-compiler-plugin.version}</version>
<configuration>
<source>${java.version}</source>
<target>${java.version}</target>
</configuration>
</plugin>
<plugin>
<groupId>org.mortbay.jetty</groupId>
<artifactId>jetty-maven-plugin</artifactId>
<version>${jetty-maven-plugin.version}</version>
<configuration>
<webAppConfig>
<contextPath>/s4c</contextPath>
</webAppConfig>
<connectors>
<connector implementation="org.eclipse.jetty.server.nio.SelectChannelConnector">
<port>9090</port>
</connector>
</connectors>
</configuration>
</plugin>
</plugins>
</build>
</project>

Model

First a User class is defined which models some basic information on a user.
package info.source4code.jsf.primefaces.model;

import java.io.Serializable;

public class User implements Serializable {

private static final long serialVersionUID = 1L;

private String userId;
private String firstName;
private String lastName;

public User(String userId, String firstName, String lastName) {
super();

this.userId = userId;
this.firstName = firstName;
this.lastName = lastName;
}

public String getUserId() {
return userId;
}

public void setUserId(String userId) {
this.userId = userId;
}

public String getFirstName() {
return firstName;
}

public void setFirstName(String firstName) {
this.firstName = firstName;
}

public String getLastName() {
return lastName;
}

public void setLastName(String lastName) {
this.lastName = lastName;
}

public String getName() {
return firstName + " " + lastName;
}

public String toString() {
return "user[userId=" + userId + ", firstName=" + firstName
+ ", lastName=" + lastName + "]";
}
}

Controller

Next we have the UserManager, which is a a session scoped managed bean that will handle all login related activities. It contains a login() method that will try to look-up a user based on a userName and userPassword combination entered on the login page. When the user is found it is assigned to the currentUser variable and a redirect to the home page is returned. If the user is not found, a redirect to the login page is returned.

A logout() method will invalidate the session and the redirect to the logout page will make sure that the previous data is longer longer available as explained in following post.

The isLoggedIn() method will be used by the LoginFilter to check if a user is logged in. It checks the value of the currentUser which is only set after a successful login. The isLoggedInForwardHome() method will return a redirect to the home page in case a user is already logged in.
Do not provide a setter for the currentUser variable, as this potentially allows a way to circumvent the login() method!
package info.source4code.jsf.primefaces.controller;

import info.source4code.jsf.primefaces.model.User;

import javax.faces.application.FacesMessage;
import javax.faces.bean.ManagedBean;
import javax.faces.bean.SessionScoped;
import javax.faces.context.FacesContext;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

@ManagedBean
@SessionScoped
public class UserManager {

private static final Logger LOGGER = LoggerFactory
.getLogger(UserManager.class);

public static final String HOME_PAGE_REDIRECT = "/secured/home.xhtml?faces-redirect=true";
public static final String LOGOUT_PAGE_REDIRECT = "/logout.xhtml?faces-redirect=true";

private String userId;
private String userPassword;
private User currentUser;

public String login() {
// lookup the user based on user name and user password
currentUser = find(userId, userPassword);

if (currentUser != null) {
LOGGER.info("login successful for '{}'", userId);

return HOME_PAGE_REDIRECT;
} else {
LOGGER.info("login failed for '{}'", userId);
FacesContext.getCurrentInstance().addMessage(
null,
new FacesMessage(FacesMessage.SEVERITY_WARN,
"Login failed",
"Invalid or unknown credentials."));

return null;
}
}

public String logout() {
String identifier = userId;

// invalidate the session
LOGGER.debug("invalidating session for '{}'", identifier);
FacesContext.getCurrentInstance().getExternalContext()
.invalidateSession();

LOGGER.info("logout successful for '{}'", identifier);
return LOGOUT_PAGE_REDIRECT;
}

public boolean isLoggedIn() {
return currentUser != null;
}

public String isLoggedInForwardHome() {
if (isLoggedIn()) {
return HOME_PAGE_REDIRECT;
}

return null;
}

private User find(String userId, String password) {
User result = null;

// code block to be replaced with actual retrieval of user
if ("john.doe".equalsIgnoreCase(userId)
&& "1234".equals(password)) {
result = new User(userId, "John", "Doe");
}

return result;
}

public String getUserId() {
return userId;
}

public void setUserId(String userId) {
this.userId = userId;
}

public String getUserPassword() {
return userPassword;
}

public void setUserPassword(String userPassword) {
this.userPassword = userPassword;
}

public User getCurrentUser() {
return currentUser;
}

// do not provide a setter for currentUser!
}

View

In the example a number of web pages will be used which realize the view part of the JSF web application. The following picture show how the different pages are structured in the webapp directory of the application WAR.
login pages overview

The login.xhtml page is shown below. The header section contains a <f:viewAction> component which will redirect to the home page in case the user is already logged in.

The body contains a <p:inputText> and <p:password> input component which allow a user to enter the user name and password. Both fields apply some basic validation in that they are both required and need a minimum length of 3 characters. The Login button will pass the entered values and call the login() method on the UserManger bean. Validation errors detected when submitting the credentials will be displayed using the <p:messages> component at the top of the page.

At the bottom of the page a number of links are included that make navigating the example easier.
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:f="http://java.sun.com/jsf/core"
xmlns:h="http://java.sun.com/jsf/html"
xmlns:ui="http://java.sun.com/jsf/facelets"
xmlns:p="http://primefaces.org/ui">

<h:head>
<title>Login</title>
<f:metadata>
<f:viewAction action="#{userManager.isLoggedInForwardHome()}" />
</f:metadata>
</h:head>

<h:body>

<h:form>
<p:messages id="messages" showDetail="false" />

<p:panel id="panel" header="Login">

<h:panelGrid columns="3" cellpadding="5">
<p:outputLabel for="userName" value="User Name:" />
<p:inputText id="userName" value="#{userManager.userId}"
required="true" label="User name">
<f:validateLength minimum="3" />
</p:inputText>
<p:message for="userName" display="icon" />

<p:outputLabel for="password" value="Password:" />
<p:password id="password"
value="#{userManager.userPassword}" required="true"
label="Password">
<f:validateLength minimum="3" />
</p:password>
<p:message for="password" display="icon" />
</h:panelGrid>

<p:commandButton value="Login" update="panel messages"
action="#{userManager.login()}" />
</p:panel>
</h:form>

<ui:include src="/WEB-INF/template/links.xhtml" />

</h:body>
</html>

The logout.xhtml page contains two <p:panel> components which are rendered based on whether a user is logged in or not. The first panel is shown in case a user is not logged in and contains a confirmation of the fact that the user is logged out. The second panel is shown in case the user is still logged in and provides a Logout button together with a reminder that the user is still logged in.
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:f="http://java.sun.com/jsf/core"
xmlns:h="http://java.sun.com/jsf/html"
xmlns:ui="http://java.sun.com/jsf/facelets"
xmlns:p="http://primefaces.org/ui">

<h:head>
<title>Logout</title>
</h:head>

<h:body>

<h:form>
<p:panel header="Logout" rendered="#{!userManager.isLoggedIn()}">
<p>You are logged out.</p>
</p:panel>

<p:panel header="Logout" rendered="#{userManager.isLoggedIn()}">
<p>You are still logged in, click below button to log
out!</p>
<p:commandButton value="Logout"
action="#{userManager.logout()}" />
</p:panel>
</h:form>

<ui:include src="/WEB-INF/template/links.xhtml" />

</h:body>
</html>

The home.xhtml page is located in a /secured folder, to which access will be protected by the LoginFilter. The page contains a basic welcome message returning the full name of the user using the getName() method. In addition a Logout button is available which allows a user to invalidate the session.
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:f="http://java.sun.com/jsf/core"
xmlns:h="http://java.sun.com/jsf/html"
xmlns:ui="http://java.sun.com/jsf/facelets"
xmlns:p="http://primefaces.org/ui">

<h:head>
<title>Home</title>
</h:head>

<h:body>

<h:form>
<p:panel header="Home">
<p>
<h:outputText
value="Welcome, #{userManager.currentUser.getName()}!" />
</p>

<p:commandButton value="Logout"
action="#{userManager.logout()}" />
</p:panel>
</h:form>

<ui:include src="/WEB-INF/template/links.xhtml" />

</h:body>
</html>

As a workaround for the fact the the LoginFilter is not applied to the files in the <welcome-file-list> of the 'web.xml', a redirect.xhtml page is added that does a simple redirect the home.xhtml using the JSF <f:viewAction> component.
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:f="http://java.sun.com/jsf/core"
xmlns:h="http://java.sun.com/jsf/html"
xmlns:ui="http://java.sun.com/jsf/facelets">

<h:head>
<title>Redirect</title>
<f:metadata>
<f:viewAction action="/secured/home.xhtml?faces-redirect=true" />
</f:metadata>
</h:head>

<h:body>

</h:body>
</html>

The unsecured.xhtml page is an example of a page that can be accesses without needing to login. The links.xhtml is a ui:composition component that is referenced from all the above page which contains links to make navigating the example easier.

Security

The LoginFilter class is a Servlet Filter that will be used to restrict access to the home page. When called, it will try to retrieve the UserManager from the ServletRequest. Note that the session attribute name used to retrieve the UserManager is the name of the managed bean. If the isLoggedIn() returns true then the call is allowed through. In all other cases, a redirect is done to the login page.
package info.source4code.jsf.primefaces.filter;

import info.source4code.jsf.primefaces.controller.UserManager;

import java.io.IOException;

import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class LoginFilter implements Filter {

private static final Logger LOGGER = LoggerFactory
.getLogger(LoginFilter.class);

public static final String LOGIN_PAGE = "/login.xhtml";

@Override
public void doFilter(ServletRequest servletRequest,
ServletResponse servletResponse, FilterChain filterChain)
throws IOException, ServletException {

HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequest;
HttpServletResponse httpServletResponse = (HttpServletResponse) servletResponse;

// managed bean name is exactly the session attribute name
UserManager userManager = (UserManager) httpServletRequest
.getSession().getAttribute("userManager");

if (userManager != null) {
if (userManager.isLoggedIn()) {
LOGGER.debug("user is logged in");
// user is logged in, continue request
filterChain.doFilter(servletRequest, servletResponse);
} else {
LOGGER.debug("user is not logged in");
// user is not logged in, redirect to login page
httpServletResponse.sendRedirect(httpServletRequest
.getContextPath() + LOGIN_PAGE);
}
} else {
LOGGER.debug("userManager not found");
// user is not logged in, redirect to login page
httpServletResponse.sendRedirect(httpServletRequest
.getContextPath() + LOGIN_PAGE);
}
}

@Override
public void init(FilterConfig arg0) throws ServletException {
LOGGER.debug("LoginFilter initialized");
}

@Override
public void destroy() {
// close resources
}
}

The 'web.xml' deployment descriptor file is shown below. It contains the definition of the LoginFilter and the resources to which it needs to be applied. In this example the filter will be applied to all pages inside the /secured directory.
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://java.sun.com/xml/ns/javaee" xmlns:web="http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
version="3.0">

<display-name>PrimeFaces Login Example</display-name>

<!-- define the JSF listener class when using the jetty-maven-plugin
with Jetty8 -->
<listener>
<listener-class>com.sun.faces.config.ConfigureListener</listener-class>
</listener>

<!-- login filter -->
<filter>
<filter-name>login</filter-name>
<filter-class>info.source4code.jsf.primefaces.filter.LoginFilter</filter-class>
</filter>
<!-- set the login filter to secure all the pages in the /secured/* path
of the application -->
<filter-mapping>
<filter-name>login</filter-name>
<url-pattern>/secured/*</url-pattern>
</filter-mapping>

<welcome-file-list>
<welcome-file>redirect.xhtml</welcome-file>
</welcome-file-list>

<servlet>
<servlet-name>faces</servlet-name>
<servlet-class>javax.faces.webapp.FacesServlet</servlet-class>
<load-on-startup>1</load-on-startup>
</servlet>

<servlet-mapping>
<servlet-name>faces</servlet-name>
<url-pattern>*.xhtml</url-pattern>
</servlet-mapping>
</web-app>

Running the Example

In order to run the above example open a command prompt and execute following Maven command:
mvn jetty:run

Maven will download the needed dependencies, compile the code and start a Jetty instance on which the web application will be deployed. Open a web browser and enter following URL: http://localhost:9090/s4c/. The result should be that below page is displayed:

login page

Enter following credentials: User name="john.doe" and Password="1234" and a welcome page will be displayed as shown below.

home page

Click the Logout button and a redirect to the logout page should happen as shown below.

logout page


github icon
If you would like to run the above code sample you can download the full source code and their corresponding JUnit test cases here.

This concludes the PrimeFaces login example using Jetty. If you found this post helpful or have any questions or remarks, please leave a comment.

Wednesday, November 26, 2014

JMS - Publish/Subscribe messaging example using ActiveMQ & Maven


In a publish/subscribe (pub/sub) product or application, clients address messages to a topic, which functions somewhat like a bulletin board. Subscribers can receive information, in the form of messages, from publishers. Topics retain messages only as long as it takes to distribute them to current subscribers. The following post introduces the basic concepts of JMS point-to-point messaging and illustrates them with a code sample using ActiveMQ and Maven.

Publish/Subscribe Messaging

Pub/sub messaging has the following characteristics:
  • Each message can have multiple consumers.
  • Publishers and subscribers have a timing dependency. A client that subscribes to a topic can consume only messages published after the client has created a subscription, and the subscriber must continue to be active in order for it to consume messages.
The JMS API relaxes this timing dependency mentioned in the second bullet to some extent by allowing subscribers to create durable subscriptions, which receive messages sent while the subscribers are not active. Durable subscriptions provide the flexibility and reliability of queues but still allow clients to send messages to many recipients.

ActiveMQ Example

Let's illustrate the above characteristics by creating a message producer that sends a message containing a first and last name to a topic. In turn a message consumer will read the message and transform it into a greeting. The code is very similar to the JMS Hello World example but contains a few key differences explained below.

Tools used:
  • ActiveMQ 5.10
  • Maven 3

The code is built and run using Maven. Specified below is the Maven POM file which contains the needed dependencies for Logback, JUnit and ActiveMQ.
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<groupId>info.source4code</groupId>
<artifactId>jms-activemq-publish-subscribe</artifactId>
<version>1.0</version>
<packaging>jar</packaging>

<name>JMS - Publish/Subscribe messaging using ActiveMQ</name>
<url>http://www.source4code.info/2014/11/jms-publish-subscribe-messaging-example-activemq-maven.html</url>

<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<java.version>1.6</java.version>

<logback.version>1.1.2</logback.version>
<slf4j.version>1.7.7</slf4j.version>
<junit.version>4.12-beta-2</junit.version>
<activemq.version>5.10.0</activemq.version>

<maven-compiler-plugin.version>3.1</maven-compiler-plugin.version>
</properties>

<dependencies>
<!-- Logging -->
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>${logback.version}</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>${slf4j.version}</version>
</dependency>
<!-- JUnit -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>${junit.version}</version>
<scope>test</scope>
</dependency>
<!-- ActiveMQ -->
<dependency>
<groupId>org.apache.activemq</groupId>
<artifactId>activemq-all</artifactId>
<version>${activemq.version}</version>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>${maven-compiler-plugin.version}</version>
<configuration>
<source>${java.version}</source>
<target>${java.version}</target>
</configuration>
</plugin>
</plugins>
</build>
</project>

Nondurable Subscription

The Publisher class contains a constructor which creates a message producer and needed connection and session objects. The sendName() operation takes as input a first and last name which are set on a TextMessage which in turn is sent to the topic set on the message producer.
package info.source4code.jms.activemq.pubsub;

import javax.jms.Connection;
import javax.jms.ConnectionFactory;
import javax.jms.JMSException;
import javax.jms.MessageProducer;
import javax.jms.Session;
import javax.jms.TextMessage;
import javax.jms.Topic;

import org.apache.activemq.ActiveMQConnection;
import org.apache.activemq.ActiveMQConnectionFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class Publisher {

private static final Logger LOGGER = LoggerFactory
.getLogger(Publisher.class);

private String clientId;
private Connection connection;
private Session session;
private MessageProducer messageProducer;

public void create(String clientId, String topicName) throws JMSException {
this.clientId = clientId;

// create a Connection Factory
ConnectionFactory connectionFactory = new ActiveMQConnectionFactory(
ActiveMQConnection.DEFAULT_BROKER_URL);

// create a Connection
connection = connectionFactory.createConnection();
connection.setClientID(clientId);

// create a Session
session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);

// create the Topic to which messages will be sent
Topic topic = session.createTopic(topicName);

// create a MessageProducer for sending messages
messageProducer = session.createProducer(topic);
}

public void closeConnection() throws JMSException {
connection.close();
}

public void sendName(String firstName, String lastName) throws JMSException {
String text = firstName + " " + lastName;

// create a JMS TextMessage
TextMessage textMessage = session.createTextMessage(text);

// send the message to the topic destination
messageProducer.send(textMessage);

LOGGER.debug(clientId + ": sent message with text='{}'", text);
}
}

The Subscriber class contains a constructor which creates a message consumer and needed connection and session objects. The getGreeting() operation reads a message from the topic and creates a greeting which is returned. A timeout parameter is passed to assure that the method does not wait indefinitely for a message to arrive.
package info.source4code.jms.activemq.pubsub;

import javax.jms.Connection;
import javax.jms.ConnectionFactory;
import javax.jms.JMSException;
import javax.jms.Message;
import javax.jms.MessageConsumer;
import javax.jms.Session;
import javax.jms.TextMessage;
import javax.jms.Topic;

import org.apache.activemq.ActiveMQConnection;
import org.apache.activemq.ActiveMQConnectionFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class Subscriber {

private static final Logger LOGGER = LoggerFactory
.getLogger(Subscriber.class);

private static final String NO_GREETING = "no greeting";

private String clientId;
private Connection connection;
private Session session;
private MessageConsumer messageConsumer;

public void create(String clientId, String topicName) throws JMSException {
this.clientId = clientId;

// create a Connection Factory
ConnectionFactory connectionFactory = new ActiveMQConnectionFactory(
ActiveMQConnection.DEFAULT_BROKER_URL);

// create a Connection
connection = connectionFactory.createConnection();
connection.setClientID(clientId);

// create a Session
session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);

// create the Topic from which messages will be received
Topic topic = session.createTopic(topicName);

// create a MessageConsumer for receiving messages
messageConsumer = session.createConsumer(topic);

// start the connection in order to receive messages
connection.start();
}

public void closeConnection() throws JMSException {
connection.close();
}

public String getGreeting(int timeout) throws JMSException {

String greeting = NO_GREETING;

// read a message from the topic destination
Message message = messageConsumer.receive(timeout);

// check if a message was received
if (message != null) {
// cast the message to the correct type
TextMessage textMessage = (TextMessage) message;

// retrieve the message content
String text = textMessage.getText();
LOGGER.debug(clientId + ": received message with text='{}'", text);

// create greeting
greeting = "Hello " + text + "!";
} else {
LOGGER.debug(clientId + ": no message received");
}

LOGGER.info("greeting={}", greeting);
return greeting;
}
}

The below JUnit test class will be used to illustrate the Pub/Sub messaging characteristics mentioned at the beginning of this post. The testGreeting() test case verifies the correct working of the getGreeting() method of the Subscriber class.

The testMultipleConsumers() test case will verify that the same message can be read by multiple consumers. In order to test this, two Subscriber instances are created on the same 'multipleconsumers.t' topic.

Finally the testNonDurableSubscriber() test case will illustrate the timing dependency between publisher and subscriber. First a message is sent to a topic on which only one subscriber listens. Then a second subscriber is added to the same topic and a second message is sent. The result is that the second subscriber only receives the second message and not the first one whereas the first subscriber has received both messages.
package info.source4code.jms.activemq.pubsub;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.fail;

import javax.jms.JMSException;

import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.junit.Test;

public class SubscriberTest {

private static Publisher publisherPublishSubscribe,
publisherMultipleConsumers, publisherNonDurableSubscriber;
private static Subscriber subscriberPublishSubscribe,
subscriber1MultipleConsumers, subscriber2MultipleConsumers,
subscriber1NonDurableSubscriber, subscriber2NonDurableSubscriber;

@BeforeClass
public static void setUpBeforeClass() throws Exception {
publisherPublishSubscribe = new Publisher();
publisherPublishSubscribe.create("publisher-publishsubscribe",
"publishsubscribe.t");

publisherMultipleConsumers = new Publisher();
publisherMultipleConsumers.create("publisher-multipleconsumers",
"multipleconsumers.t");

publisherNonDurableSubscriber = new Publisher();
publisherNonDurableSubscriber.create("publisher-nondurablesubscriber",
"nondurablesubscriber.t");

subscriberPublishSubscribe = new Subscriber();
subscriberPublishSubscribe.create("subscriber-publishsubscribe",
"publishsubscribe.t");

subscriber1MultipleConsumers = new Subscriber();
subscriber1MultipleConsumers.create("subscriber1-multipleconsumers",
"multipleconsumers.t");

subscriber2MultipleConsumers = new Subscriber();
subscriber2MultipleConsumers.create("subscriber2-multipleconsumers",
"multipleconsumers.t");

subscriber1NonDurableSubscriber = new Subscriber();
subscriber1NonDurableSubscriber.create(
"subscriber1-nondurablesubscriber", "nondurablesubscriber.t");

subscriber2NonDurableSubscriber = new Subscriber();
subscriber2NonDurableSubscriber.create(
"subscriber2-nondurablesubscriber", "nondurablesubscriber.t");
}

@AfterClass
public static void tearDownAfterClass() throws Exception {
publisherPublishSubscribe.closeConnection();
publisherMultipleConsumers.closeConnection();
publisherNonDurableSubscriber.closeConnection();

subscriberPublishSubscribe.closeConnection();
subscriber1MultipleConsumers.closeConnection();
subscriber2MultipleConsumers.closeConnection();
subscriber1NonDurableSubscriber.closeConnection();
subscriber2NonDurableSubscriber.closeConnection();
}

@Test
public void testGetGreeting() {
try {
publisherPublishSubscribe.sendName("Peregrin", "Took");

String greeting1 = subscriberPublishSubscribe.getGreeting(1000);
assertEquals("Hello Peregrin Took!", greeting1);

String greeting2 = subscriberPublishSubscribe.getGreeting(1000);
assertEquals("no greeting", greeting2);

} catch (JMSException e) {
fail("a JMS Exception occurred");
}
}

@Test
public void testMultipleConsumers() {
try {
publisherMultipleConsumers.sendName("Gandalf", "the Grey");

String greeting1 = subscriber1MultipleConsumers.getGreeting(1000);
assertEquals("Hello Gandalf the Grey!", greeting1);

String greeting2 = subscriber2MultipleConsumers.getGreeting(1000);
assertEquals("Hello Gandalf the Grey!", greeting2);

} catch (JMSException e) {
fail("a JMS Exception occurred");
}
}

@Test
public void testNonDurableSubscriber() {
try {
// nondurable subscriptions, will not receive messages sent while
// the subscribers are not active
subscriber2NonDurableSubscriber.closeConnection();

publisherNonDurableSubscriber.sendName("Bilbo", "Baggins");

// recreate a connection for the nondurable subscription
subscriber2NonDurableSubscriber.create(
"subscriber2-nondurablesubscriber",
"nondurablesubscriber.t");

publisherNonDurableSubscriber.sendName("Frodo", "Baggins");

String greeting1 = subscriber1NonDurableSubscriber
.getGreeting(1000);
assertEquals("Hello Bilbo Baggins!", greeting1);
String greeting2 = subscriber1NonDurableSubscriber
.getGreeting(1000);
assertEquals("Hello Frodo Baggins!", greeting2);

String greeting3 = subscriber2NonDurableSubscriber
.getGreeting(1000);
assertEquals("Hello Frodo Baggins!", greeting3);
String greeting4 = subscriber2NonDurableSubscriber
.getGreeting(1000);
assertEquals("no greeting", greeting4);

} catch (JMSException e) {
fail("a JMS Exception occurred");
}
}
}

Make sure a default ActiveMQ message broker is up and running, open a command prompt and execute following Maven command:
mvn -Dtest=SubscriberTest test

This will trigger Maven to run the above test cases which should result in the following log statements.
07:24:00.299 DEBUG [main][Publisher]
publisher-multipleconsumers: sent message with text='Gandalf the Grey'
07:24:00.303 DEBUG [main][Subscriber]
subscriber1-multipleconsumers: received message with text='Gandalf the Grey'
07:24:00.303 INFO [main][Subscriber]
greeting=Hello Gandalf the Grey!
07:24:00.304 DEBUG [main][Subscriber]
subscriber2-multipleconsumers: received message with text='Gandalf the Grey'
07:24:00.304 INFO [main][Subscriber]
greeting=Hello Gandalf the Grey!
07:24:00.306 DEBUG [main][Publisher]
publisher-publishsubscribe: sent message with text='Peregrin Took'
07:24:00.306 DEBUG [main][Subscriber]
subscriber-publishsubscribe: received message with text='Peregrin Took'
07:24:00.307 INFO [main][Subscriber]
greeting=Hello Peregrin Took!
07:24:01.307 DEBUG [main][Subscriber]
subscriber-publishsubscribe: no message received
07:24:01.307 INFO [main][Subscriber]
greeting=no greeting
07:24:01.320 DEBUG [main][Publisher]
publisher-nondurablesubscriber: sent message with text='Bilbo Baggins'
07:24:01.337 DEBUG [main][Publisher]
publisher-nondurablesubscriber: sent message with text='Frodo Baggins'
07:24:01.338 DEBUG [main][Subscriber]
subscriber1-nondurablesubscriber: received message with text='Bilbo Baggins'
07:24:01.338 INFO [main][Subscriber]
greeting=Hello Bilbo Baggins!
07:24:01.338 DEBUG [main][Subscriber]
subscriber1-nondurablesubscriber: received message with text='Frodo Baggins'
07:24:01.338 INFO [main][Subscriber]
greeting=Hello Frodo Baggins!
07:24:01.339 DEBUG [main][Subscriber]
subscriber2-nondurablesubscriber: received message with text='Frodo Baggins'
07:24:01.339 INFO [main][Subscriber]
greeting=Hello Frodo Baggins!
07:24:02.339 DEBUG [main][Subscriber]
subscriber2-nondurablesubscriber: no message received
07:24:02.339 INFO [main][Subscriber]
greeting=no greeting

Durable Subscription

As mentioned in the beginning of this post it is also possible to create a durable subscription which allows to receive messages sent while the subscribers are not active. The JMS specification dictates that the identification of a specific durable subscription is done by a combination of the client ID, the durable subscription name and the topic name.

As a result the below DurableSubscriber has three main differences with the previous Subscriber class:
  • A clientId is mandatory on the connection in order to allow a JMS provider to uniquely identify a durable subscriber.
  • A durable subscriber is created using Session.CreateDurableSubscriber.
  • A subscriptionName is needed when creating the durable subscriber.
Note that creating a MessageConsumer provides the same features as creating a TopicSubscriber. The TopicSubscriber is provided to support existing code.
    package info.source4code.jms.activemq.pubsub;

    import javax.jms.Connection;
    import javax.jms.ConnectionFactory;
    import javax.jms.JMSException;
    import javax.jms.Message;
    import javax.jms.MessageConsumer;
    import javax.jms.Session;
    import javax.jms.TextMessage;
    import javax.jms.Topic;

    import org.apache.activemq.ActiveMQConnection;
    import org.apache.activemq.ActiveMQConnectionFactory;
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;

    public class DurableSubscriber {

    private static final Logger LOGGER = LoggerFactory
    .getLogger(DurableSubscriber.class);

    private static final String NO_GREETING = "no greeting";

    private String clientId;
    private Connection connection;
    private Session session;
    private MessageConsumer messageConsumer;

    private String subscriptionName;

    public void create(String clientId, String topicName,
    String subscriptionName) throws JMSException {
    this.clientId = clientId;
    this.subscriptionName = subscriptionName;

    // create a Connection Factory
    ConnectionFactory connectionFactory = new ActiveMQConnectionFactory(
    ActiveMQConnection.DEFAULT_BROKER_URL);

    // create a Connection
    connection = connectionFactory.createConnection();
    connection.setClientID(clientId);

    // create a Session
    session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);

    // create the Topic from which messages will be received
    Topic topic = session.createTopic(topicName);

    // create a MessageConsumer for receiving messages
    messageConsumer = session.createDurableSubscriber(topic,
    subscriptionName);

    // start the connection in order to receive messages
    connection.start();
    }

    public void removeDurableSubscriber() throws JMSException {
    messageConsumer.close();
    session.unsubscribe(subscriptionName);
    }

    public void closeConnection() throws JMSException {
    connection.close();
    }

    public String getGreeting(int timeout) throws JMSException {

    String greeting = NO_GREETING;

    // read a message from the topic destination
    Message message = messageConsumer.receive(timeout);

    // check if a message was received
    if (message != null) {
    // cast the message to the correct type
    TextMessage textMessage = (TextMessage) message;

    // retrieve the message content
    String text = textMessage.getText();
    LOGGER.debug(clientId + ": received message with text='{}'", text);

    // create greeting
    greeting = "Hello " + text + "!";
    } else {
    LOGGER.debug(clientId + ": no message received");
    }

    LOGGER.info("greeting={}", greeting);
    return greeting;
    }
    }

    The below JUnit test class will be used to illustrate the durable subscriber messaging characteristics. It contains a testDurableSubscriber() test case that will first remove one of the two durable subscribers that are listening on the 'durablesubscriber.t' topic by closing it's connection to the broker. Then a first message is sent to this topic on which only one subscribers is still actively listening. The second subscriber is recreated using the same client ID and subscription name and a second message is sent. The expected result is that both subscribers receive the two messages.
    Note that in the tearDownAfterClass() method the durable subscriptions are removed in order to avoid an error when rerunning the test case.
    package info.source4code.jms.activemq.pubsub;

    import static org.junit.Assert.assertEquals;
    import static org.junit.Assert.fail;

    import javax.jms.JMSException;

    import org.junit.AfterClass;
    import org.junit.BeforeClass;
    import org.junit.Test;

    public class DurableSubscriberTest {

    private static Publisher publisherPublishSubscribe,
    publisherDurableSubscriber;
    private static DurableSubscriber subscriberPublishSubscribe,

    subscriber1DurableSubscriber, subscriber2DurableSubscriber;

    @BeforeClass
    public static void setUpBeforeClass() throws Exception {
    publisherPublishSubscribe = new Publisher();
    publisherPublishSubscribe.create("publisher-publishsubscribe",
    "publishsubscribe.t");

    publisherDurableSubscriber = new Publisher();
    publisherDurableSubscriber.create("publisher-durablesubscriber",
    "durablesubscriber.t");

    subscriberPublishSubscribe = new DurableSubscriber();
    subscriberPublishSubscribe.create("subscriber-publishsubscribe",
    "publishsubscribe.t", "publishsubscribe");

    subscriber1DurableSubscriber = new DurableSubscriber();
    subscriber1DurableSubscriber.create("subscriber1-durablesubscriber",
    "durablesubscriber.t", "durablesubscriber1");

    subscriber2DurableSubscriber = new DurableSubscriber();
    subscriber2DurableSubscriber.create("subscriber2-durablesubscriber",
    "durablesubscriber.t", "durablesubscriber2");
    }

    @AfterClass
    public static void tearDownAfterClass() throws Exception {
    publisherPublishSubscribe.closeConnection();
    publisherDurableSubscriber.closeConnection();

    // remove the durable subscriptions
    subscriberPublishSubscribe.removeDurableSubscriber();
    subscriber1DurableSubscriber.removeDurableSubscriber();
    subscriber2DurableSubscriber.removeDurableSubscriber();

    subscriberPublishSubscribe.closeConnection();
    subscriber1DurableSubscriber.closeConnection();
    subscriber2DurableSubscriber.closeConnection();
    }

    @Test
    public void testGetGreeting() {
    try {
    publisherPublishSubscribe.sendName("Peregrin", "Took");

    String greeting1 = subscriberPublishSubscribe.getGreeting(1000);
    assertEquals("Hello Peregrin Took!", greeting1);

    String greeting2 = subscriberPublishSubscribe.getGreeting(1000);
    assertEquals("no greeting", greeting2);

    } catch (JMSException e) {
    fail("a JMS Exception occurred");
    }
    }

    @Test
    public void testDurableSubscriber() {
    try {
    // durable subscriptions, receive messages sent while the
    // subscribers are not active
    subscriber2DurableSubscriber.closeConnection();

    publisherDurableSubscriber.sendName("Bilbo", "Baggins");

    // recreate a connection for the durable subscription
    subscriber2DurableSubscriber.create(
    "subscriber2-durablesubscriber", "durablesubscriber.t",
    "durablesubscriber2");

    publisherDurableSubscriber.sendName("Frodo", "Baggins");

    String greeting1 = subscriber1DurableSubscriber.getGreeting(1000);
    assertEquals("Hello Bilbo Baggins!", greeting1);
    String greeting2 = subscriber2DurableSubscriber.getGreeting(1000);
    assertEquals("Hello Bilbo Baggins!", greeting2);

    String greeting3 = subscriber1DurableSubscriber.getGreeting(1000);
    assertEquals("Hello Frodo Baggins!", greeting3);
    String greeting4 = subscriber2DurableSubscriber.getGreeting(1000);
    assertEquals("Hello Frodo Baggins!", greeting4);

    } catch (JMSException e) {
    fail("a JMS Exception occurred");
    }
    }
    }

    Make sure a default ActiveMQ message broker is up and running, open a command prompt and execute following Maven command:
    mvn -Dtest=DurableSubscriberTest test

    This will trigger Maven to run the above test cases which should result in the following log statements.
    18:58:54.591 DEBUG [main][Publisher]
    publisher-durablesubscriber: sent message with text='Bilbo Baggins'
    18:58:54.632 DEBUG [main][Publisher]
    publisher-durablesubscriber: sent message with text='Frodo Baggins'
    18:58:54.633 DEBUG [main][DurableSubscriber]
    subscriber1-durablesubscriber: received message with text='Bilbo Baggins'
    18:58:54.634 INFO [main][DurableSubscriber]
    greeting=Hello Bilbo Baggins!
    18:58:54.635 DEBUG [main][DurableSubscriber]
    subscriber2-durablesubscriber: received message with text='Bilbo Baggins'
    18:58:54.635 INFO [main][DurableSubscriber]
    greeting=Hello Bilbo Baggins!
    18:58:54.636 DEBUG [main][DurableSubscriber]
    subscriber1-durablesubscriber: received message with text='Frodo Baggins'
    18:58:54.636 INFO [main][DurableSubscriber]
    greeting=Hello Frodo Baggins!
    18:58:54.636 DEBUG [main][DurableSubscriber]
    subscriber2-durablesubscriber: received message with text='Frodo Baggins'
    18:58:54.637 INFO [main][DurableSubscriber]
    greeting=Hello Frodo Baggins!
    18:58:54.669 DEBUG [main][Publisher]
    publisher-publishsubscribe: sent message with text='Peregrin Took'
    18:58:54.670 DEBUG [main][DurableSubscriber]
    subscriber-publishsubscribe: received message with text='Peregrin Took'
    18:58:54.670 INFO [main][DurableSubscriber]
    greeting=Hello Peregrin Took!
    18:58:55.670 DEBUG [main][DurableSubscriber]
    subscriber-publishsubscribe: no message received
    18:58:55.670 INFO [main][DurableSubscriber]
    greeting=no greeting


    github icon
    If you would like to run the above code sample you can download the full source code and their corresponding JUnit test cases here.

    This concludes the JMS publish/subscribe example using ActiveMQ. If you found this post helpful or have any questions or remarks, please leave a comment.