Logging SOAP client messages from CXF in Spring Boot

Since getting this to work was more troublesome that it should have been, here are the details.

First, the following dependencies will be required (I am using 2.7.18):

<dependency>
	<groupId>org.apache.cxf</groupId>
	<artifactId>cxf-api</artifactId>
	<version>${cxf.runtime.version}</version>
</dependency>
<dependency>
	<groupId>org.apache.cxf</groupId>
	<artifactId>cxf-rt-transports-http</artifactId>
	<version>${cxf.runtime.version}</version>
</dependency>
<dependency>
	<groupId>org.apache.cxf</groupId>
	<artifactId>cxf-rt-frontend-jaxws</artifactId>
	<version>${cxf.runtime.version}</version>
</dependency>

Next, we need a Spring configuration class that sets up the required CXF classes:


@Configuration
@ImportResource({ "classpath:META-INF/cxf/cxf.xml", "classpath:META-INF/cxf/cxf-extension-soap.xml" })
public class CxfConfig {

	@Autowired
	private SpringBus springBus;

	@PostConstruct
	public void activateLoggingFeature() {
           springBus.getInInterceptors().add(logInInterceptor());
    	   springBus.getInFaultInterceptors().add(logInInterceptor());
    	   springBus.getOutInterceptors().add(logOutInterceptor());
    	   springBus.getOutFaultInterceptors().add(logOutInterceptor());
	}

	@Bean
	public LoggingFeature loggingFeature() {
    	   LoggingFeature logFeature = new LoggingFeature();
    	   logFeature.setPrettyLogging(true);
    	   logFeature.initialize(springBus);
    	   springBus.getFeatures().add(logFeature);
    	   return logFeature;
	}

	public AbstractLoggingInterceptor logInInterceptor() {
	    return new LoggingInInterceptor();
	}

	public AbstractLoggingInterceptor logOutInterceptor() {
		LoggingOutInterceptor logOutInterceptor = new LoggingOutInterceptor();
		logOutInterceptor.setPrettyLogging(true);
		return logOutInterceptor;
	}

}

Finally, we need to add CXF components to the logging framework config (e.g. logback.xml)

...
   <logger name="org.apache.cxf" level="INFO"/>
...
---------------------------
ID: 12
Address: ... something ....
Encoding: UTF-8
Http-Method: POST
Content-Type: text/xml
Headers: {Accept=[*/*], SOAPAction=[""]}
Payload: <soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
  <soap:Body>

     ... data here ....

  </soap:Body>
</soap:Envelope>

--------------------------------------

Unable to find contextual data of type: javax.servlet.ServletContext

The combination of Spring Boot, RestEasy and Swagger 2 is not exactly a happy combination. Both Spring Boot and Swagger seems to prefer Jersey to RestEasy.  However, in my case as in others, RestEasy is mandated by factors outside my control.

Starting with Andy Wilkinson’s Spring Boot RestEasy (https://github.com/wilkinsona/spring-boot-resteasy), and then adding the latest Swagger 2 libraries (io.swagger), I was plagued by the error: Unable to find contextual data of type: javax.servlet.ServletContext

It seems the new io.swagger.jaxrs.listing.ApiListingResource causes RestEasy 3.0.9 to require a ThreadLocal reference to ServletContext which basically is not there, hence the error, which does not occur with the older com.wordnik.swagger.jaxrs.listing.ApiListingResource.

To get around this, I basically had to supply RestEasy with a thread local ServletContext myself, using a Jax-rs ContainerRequestFilter. The full project source is here: https://github.com/aosullivan/springboot-resteasy-swagger

The key Filter is as follows:

@ConditionalOnWebApplication
@Component
@Provider
@ServerInterceptor
public class ResteasyServletContext implements ContainerRequestFilter {

    @Inject 
    ServletContext sc;
    
    @Override
    public void filter(ContainerRequestContext requestContext) throws IOException {
		ResteasyProviderFactory.getContextDataMap().put(ServletContext.class, sc);
    }

}

Following this, everything seemed to work, and I could bring up swagger-ui and access the Jax-rs endpoints through it.

This condition is needed to support the spring SpringJUnit4ClassRunner, if you use that, or other contexts where the web container is not actually instantiated:

@ConditionalOnWebApplication

I get CORS issues when accessing swagger-ui from Chrome, which I can get around using the Chrome CORS plugin, https://chrome.google.com/webstore/detail/allow-control-allow-origi/nlfbmbojpeacfghkpbjhddihlkkiljbi?hl=en.  This didn’t happen under Jersey.

Configuring DateFormat with Spring Boot and RestEasy

This is so finely tuned, I pity the fool who has to make it work (me).

A couple of things to check. First, ensure you Application (with main method) is in a package a level above components to be autowired, especially the @Producer. Also, ensure it has these annotations:

@ComponentScan
@SpringBootApplication

Second, create a configuration type which is both a @Provider and a @Component.

Ensure that class imports of configuration classes such as ObjectMap are only from the package com.fasterxml.jackson. Don’t import anything from Jackson1, org.codehaus.jackson. Don’t mix Jackson1 and 2 and keep 1 out of the implementation.

I’m including the imports so there is no ambiguity about this.

import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.datatype.joda.JodaMapper;

import javax.ws.rs.ext.ContextResolver;
import javax.ws.rs.ext.Provider;

import org.springframework.stereotype.Component;

import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.TimeZone;

@Provider
@Component
public class DateObjectMapperProvider implements ContextResolver<ObjectMapper> {
    private DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");

    @Override
    public ObjectMapper getContext(Class<?> type) {
        dateFormat.setLenient(false);
        dateFormat.setTimeZone(TimeZone.getTimeZone("PST"));
        
        final ObjectMapper result = new JodaMapper();
        result.setDateFormat(dateFormat);
        return result;
    }

}

Maven dependencies, including Swagger. I have not included Spring.


<swagger.version>1.3.12</swagger.version>
<resteasy.version>3.0.9.Final</resteasy.version>
...
                         <dependency>
				<groupId>org.jboss.resteasy</groupId>
				<artifactId>resteasy-spring</artifactId>
				<version>${resteasy.version}</version>
			</dependency>
			<dependency>
				<groupId>org.jboss.resteasy</groupId>
				<artifactId>resteasy-jackson2-provider</artifactId>
				<version>${resteasy.version}</version>
			</dependency>
			<dependency>
				<groupId>org.jboss.resteasy</groupId>
				<artifactId>resteasy-validator-provider-11</artifactId>
				<version>${resteasy.version}</version>
			</dependency>
			<dependency>
				<groupId>org.jboss.resteasy</groupId>
				<artifactId>resteasy-multipart-provider</artifactId>
				<version>${resteasy.version}</version>
			</dependency>
			<dependency>
				<groupId>org.jboss.resteasy</groupId>
				<artifactId>resteasy-jaxb-provider</artifactId>
				<version>${resteasy.version}</version>
			</dependency>

			<dependency>
				<groupId>com.wordnik</groupId>
				<artifactId>swagger-jaxrs_2.10</artifactId>
				<version>${swagger.version}</version>
			</dependency>
			<dependency>
				<groupId>com.wordnik</groupId>
				<artifactId>swagger-core_2.10</artifactId>
				<version>${swagger.version}</version>
			</dependency>
			
			<dependency>
				<groupId>com.mangofactory</groupId>
				<artifactId>swagger-springmvc</artifactId>
				<version>1.0.2</version>
			</dependency>
	
			<dependency>
				<groupId>org.ajar</groupId>
				<artifactId>swagger-spring-mvc-ui</artifactId>
				<version>0.4</version>
			</dependency>			

		<dependency>
		<groupId>com.fasterxml.jackson.datatype</groupId>
			<artifactId>jackson-datatype-joda</artifactId>
		</dependency>
		<dependency>
			<groupId>com.fasterxml.jackson.core</groupId>
			<artifactId>jackson-databind</artifactId>
		</dependency>

Set it up this way and you should be golden.

Sonar Unit and Integration Test Coverage with Maven

Lots of posts on the web about this, few seem to work. Here is my config, which works.

First, configuration of the maven sonar plugin:

<sonar.dynamicAnalysis>reuseReports</sonar.dynamicAnalysis>  <!-- Ensure you run mvn install before sonar:sonar -->
<sonar.java.codeCoveragePlugin>jacoco</sonar.java.codeCoveragePlugin>
<sonar.surefire.reportsPath>/target/surefire-reports</sonar.surefire.reportsPath>
<sonar.jacoco.reportPath>target/jacoco.exec</sonar.jacoco.reportPath>    <!-- This is the default, put here to be explicit -->
<sonar.jacoco.itReportPath>target/jacoco-it.exec</sonar.jacoco.itReportPath>

Next, the Jacoco plugin. Here we use the default output file for unit tests, and a separate output file for integration tests:

<plugin>
    <groupId>org.jacoco</groupId>
    <artifactId>jacoco-maven-plugin</artifactId>
    <version>0.6.2.201302030002</version>
    <executions>
        <execution>
            <id>pre-unit-test</id>
            <goals>
                <goal>prepare-agent</goal>
            </goals>
        </execution>
        <execution>
            <id>post-unit-test</id>
            <phase>test</phase>
            <goals>
                <goal>report</goal>
            </goals>
        </execution>
        <execution>
            <id>pre-integration-test</id>
            <phase>pre-integration-test</phase>
            <goals>
                <goal>prepare-agent</goal>
            </goals>
            <configuration>
                <destFile>target/jacoco-it.exec</destFile>
                <propertyName>failsafe.argLine</propertyName>
            </configuration>
        </execution>
        <execution>
            <id>post-integration-test</id>
            <phase>post-integration-test</phase>
            <goals>
                <goal>report</goal>
            </goals>
            <configuration>
                <dataFile>target/jacoco-it.exec</dataFile>
            </configuration>
        </execution>
    </executions>
</plugin>

Finally, the failsafe plugin. This ensures tests will be instrumented during the integration test phase, and the results collected during the verify phase. The reference to argLine is critical, because this causes failsafe to write to the correct Jacoco output file.

<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-failsafe-plugin</artifactId>
    <version>2.14</version>
    <configuration>
        <argLine>${failsafe.argLine}</argLine>
    </configuration>
    <executions>
        <execution>
            <id>integration-test</id>
            <goals>
                <goal>integration-test</goal>
            </goals>
        </execution>
        <execution>
            <id>verify</id>
            <goals>
                <goal>verify</goal>
            </goals>
        </execution>
    </executions>
</plugin>

JiraRestClient for Java does not support edits or updates

Continuing from my last post on using Jira’s REST client for Java, it seems the version I am using, 2.0.0-m2, does not actually support updates.  It is read only.

Because it’s not like you’d ever want to update JIRA through its API, obviously :O

However the basic REST api does support updates.  To use this you need to authenticate and do an HTTP PUT which contains a JSON message with the update details.  Here I am going to post the code to update a custom field referenced by name.  Took me all afternoon.  Atlassian suck.

public class JiraRestAPITest {
  private static final String JIRA_USERNAME = "admin";
  private static final String JIRA_PASSWORD = "password";
  private static final String JIRA_URL = "http://localhost:8091/";
  private static final String CUSTOM_FIELD_NAME = "AosCustom1";
  private static final String JIRA_FILTER_BY_STATUS = "Open";
  private static final String JIRA_PROJECT_NAME = "aosdemo";
  private static final String ISSUE_ID = "DEMO-3";

  @Test
  public void testSomething() throws URISyntaxException {

    JiraRestClient restClient = login(JIRA_USERNAME, JIRA_PASSWORD);
    String jsql = filterByProject(JIRA_PROJECT_NAME);

    SearchResult results = restClient.getSearchClient().searchJql(jsql).claim();
    for (BasicIssue i : results.getIssues()) {
       String key = i.getKey();

       Issue issue = restClient.getIssueClient().getIssue(key).claim();
       Field customField = issue.getFieldByName(CUSTOM_FIELD_NAME);

       if (ISSUE_ID.equals(key)){
          String newValue = "Edited by AdrianOS at " + new Date();
          update(issue, customField, newValue);
       }

    }

  }

  private static String filterByProject(String projectName) {
    return String.format("project = '%s' AND status = '%s' ORDER BY priority DESC", projectName, JIRA_FILTER_BY_STATUS);
  }

  private static void update(Issue issue, Field field, String value) {

     try {
       HttpURLConnection connection = urlConnection(forIssue(issue), withEncoding());

       connection.connect();

       writeData(connection, jsonEditIssue(field, value));
       checkResponse(connection);

    } catch (IOException e) {
       throw new RuntimeException(e);
    }

 }

 private static void writeData(HttpURLConnection connection, JSONObject fields) throws IOException {
    System.out.println(fields);
    OutputStreamWriter out = new OutputStreamWriter(connection.getOutputStream());
    out.write(fields.toString());
    out.flush();
    out.close();
 }

 private static String withEncoding() {
    String userPassword = JIRA_USERNAME + ":" + JIRA_PASSWORD;
    return encodeBase64String(userPassword.getBytes());
 }

 private static void checkResponse(HttpURLConnection connection)
 throws IOException {

    if (HttpURLConnection.HTTP_NO_CONTENT != connection.getResponseCode()) {

    BufferedReader reader = new BufferedReader(new InputStreamReader(
    connection.getInputStream()));
    StringBuilder stringBuilder = new StringBuilder();

    String line = null;
    while ((line = reader.readLine()) != null) {
       stringBuilder.append(line + "\n");
    }

    System.err.println(stringBuilder.toString());
    }
 }

 private static URL forIssue(Issue issue) throws MalformedURLException {
    return issue.getSelf().toURL();
 }

 private static HttpURLConnection urlConnection(URL url, String encoding) throws IOException {
    HttpURLConnection connection = (HttpURLConnection)url.openConnection();
    connection.setDoOutput(true);
    connection.setRequestProperty("Content-Type", "application/json");
    connection.setRequestMethod("PUT");
    connection.setRequestProperty("Authorization", "Basic " + encoding);
    return connection;
 }

 private static JSONObject jsonEditIssue(Field field, String value) {
    JSONObject summary = new JSONObject()
      .accumulate(field.getId(), value);

    JSONObject fields = new JSONObject().accumulate("fields", summary);
      return fields;
 }

 private static JiraRestClient login(String admin, String password) throws URISyntaxException {
    final URI jiraServerUri = new URI(JIRA_URL);
    final JiraRestClientFactory factory = new AsynchronousJiraRestClientFactory();
    return factory.createWithBasicHttpAuthentication(jiraServerUri, admin, password);
 }

}

Here are the dependencies:

 	<dependencies>
		<dependency>
			<groupId>com.atlassian.jira</groupId>
			<artifactId>jira-rest-java-client</artifactId>
			<version>2.0.0-m2</version>
		</dependency>

		<dependency>
			<groupId>net.sf.json-lib</groupId>
			<artifactId>json-lib</artifactId>
			<version>2.4</version>
			<classifier>jdk15</classifier>
		</dependency>

		<dependency>
			<groupId>commons-codec</groupId>
			<artifactId>commons-codec</artifactId>
			<version>1.6</version>
		</dependency>

		<dependency>
			<groupId>junit</groupId>
			<artifactId>junit</artifactId>
			<version>4.11</version>
			<scope>test</scope>
		</dependency>
	</dependencies>

Should I test hashCode(), equals(), toString()?

The Java language provides some default functions on objects that we usually want to override:

toString()
equals()
hashCode()

This is plumbing; the methods can’t fulfil any business requirements in themselves, but Java developers generally put them into objects early on. In a way this is a violation of YAGNI, which goes against the TDD grain.

Watching James Shore’s test driven development videos, I came across an interesting approach to implementing these methods. James begins with a single test, valueObject(), to check these functions:

@Test
public void valueObject() {
  Year year1a = new Year(2010);
  Year year1b = new Year(2010);
  Year year2 = new Year(2012);
   
  assertEquals("2010", year1a.toString());
  assertTrue("years with same value should be equal", year1a.equals(year1b));
  assertFalse("years with different values should not be equal", year1a.equals(year2));
  assertTrue("years with same value should have same hashcode", year1a.hashCode() == year1b.hashCode());
}

He then makes the tests compile by implementing the class.

package com.jamesshore.finances.domain;

public class Year {
  int year;
}

And then uses the Eclipse tool for autogenerating equals, hashCode and toString:

public String toString() {
  return "" + year;
}

@Override
public int hashCode() {
  final int prime = 31;
  int result = 1;
  result = prime * result + year;
  return result;
}

@Override
public boolean equals(Object obj) {
  if (this == obj) return true;
  if (obj == null) return false;
  if (getClass() != obj.getClass()) return false;
  Year other = (Year) obj;
  if (year != other.year) return false;
  return true;
}

Now the tests pass.  Its interesting how the boilerplate code of the hashCode() etc. requires boilerplate code in the test.  Though it violated YAGNI, most Java developers I know prefer to create these methods up front, and this seems an easy way to handle it.

These methods are generally only needed on value object (i.e. objects where equality is important) or entity objects (where identity is important).  Developers generally know when they create classes whether they are value or entity objects, so it makes sense to me to create these methods up front.