Thursday, June 10, 2010

CXF|How to create a WSDL-first SOAP client in Java with CXF and Maven

How to create a WSDL-first SOAP client in Java with CXF and Maven

Posted on October 19, 2008. Filed under: Java Programming |

Background

About a week ago I needed to write a SOAP-based client for work. The SOAP framework I'm using is Apache CXF. I'm a total noob when it comes to SOAP services, and so I was a little apprehensive about this at first. My apprehension sprung from hearing horror stories a few years ago from coworkers who were writing Axis SOAP applications, and they were basically tearing their hair out over Axis.

However, word has it that CXF is much easier to use. Well, it took me a while to get it working correctly. In an effort to save other folks the same grief, I've posted my code here. If you're reading this, I'm assuming you're acquainted with Java and Maven, but fairly new to SOAP, WSDL, etc.

To keep things simple, I decided to write a "Hello World" type of application first to make sure I could get the technology stack working correctly. To keep things really simple, I decided to create a trivial Java "main" function that calls a SOAP service, logs the result to the console, and exits (no fancy web interface or anything like that).

The SOAP Service Provider

First I had to select an appropriate web service to test against. There are a bunch of free SOAP-based web services out there, and I chose the CDyne weather service. You can go read all about it if you want.

Obtain the Service's WSDL

When you're writing a new client in CXF for an existing web service, you start with the WSDL and work from there. This means you need to get a copy of the WSDL from the service provider. The WSDL for the CDyne weather service can be downloaded from their site. You can simply right-click that link and save the WSDL on your hard drive.

Once you have the WSDL in hand, you can build your client around it. Basically, you'll use a CXF tool called wsdl2java to turn the WSDL into Java stub code that you then compile along with your application.

Create the Maven Project

As a recent convert to Maven, I set up a new Maven project. I created a new project directory called weather-client, which is the ${basedir}. Also, I put the WSDL file in ${basedir}/src/main/wsdl/weather.wsdl.

Yeah, I know Maven has its fancy archetype creator thingie to emit the initial POM file, but like most pragmatic programmers I simply copy and paste a similar POM from somewhere else and modify it to suit my needs. Here's the project file I came up with.

weather-client/pom.xml

002    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
003 
005 
006    <modelVersion>4.0.0</modelVersion>
007    <groupId>com.logicsector</groupId>
008    <artifactId>weather-client</artifactId>
009    <version>1.0</version>
010    <name>SOAP weather client</name>
011    <packaging>jar</packaging>
012 
013    <dependencies>
014        <dependency>
015             <groupId>org.apache.cxf</groupId>
016            <artifactId>cxf-rt-frontend-jaxws</artifactId>
017            <version>2.1.2</version>
018        </dependency>
019         <dependency>
020             <groupId>org.apache.cxf</groupId>
021            <artifactId>cxf-rt-transports-http</artifactId>
022            <version>2.1.2</version>
023        </dependency>
024         <dependency>
025             <groupId>org.slf4j</groupId>
026            <artifactId>slf4j-api</artifactId>
027            <version>1.5.2</version>
028        </dependency>
029         <dependency>
030             <groupId>org.slf4j</groupId>
031            <artifactId>slf4j-log4j12</artifactId>
032            <version>1.5.2</version>
033        </dependency>
034     </dependencies>
035 
036    <build>
037        <finalName>weather-client</finalName>
038        <plugins>
039             <!-- Generate Java classes from WSDL during build -->
040            <plugin>
041                 <groupId>org.apache.cxf</groupId>
042                <artifactId>cxf-codegen-plugin</artifactId>
043                <version>2.1.2</version>
044                <executions>
045                    <execution>
046                        <id>generate-sources</id>
047                        <phase>generate-sources</phase>
048                        <configuration>
049                            <sourceRoot>${basedir}/target/generated/src/main/java</sourceRoot>
050                            <wsdlOptions>
051                                <wsdlOption>
052                                    <wsdl>${basedir}/src/main/wsdl/weather.wsdl</wsdl>
053                                    <extraargs>
054                                        <extraarg>-client</extraarg>
055                                    </extraargs>
056                                </wsdlOption>
057                            </wsdlOptions>
058                        </configuration>
059                        <goals>
060                            <goal>wsdl2java</goal>
061                        </goals>
062                    </execution>
063                </executions>
064            </plugin>
065            <!-- Add generated sources - avoids having to copy generated sources to build location -->
066            <plugin>
067                <groupId>org.codehaus.mojo</groupId>
068                <artifactId>build-helper-maven-plugin</artifactId>
069                <executions>
070                    <execution>
071                        <id>add-source</id>
072                        <phase>generate-sources</phase>
073                        <goals>
074                            <goal>add-source</goal>
075                        </goals>
076                        <configuration>
077                            <sources>
078                                <source>${basedir}/target/generated/src/main/java</source>
079                            </sources>
080                        </configuration>
081                    </execution>
082                </executions>
083            </plugin>
084            <!-- Build the JAR with dependencies -->
085            <plugin>
086                <artifactId>maven-assembly-plugin</artifactId>
087                <configuration>
088                    <descriptorRefs>
089                        <descriptorRef>jar-with-dependencies</descriptorRef>
090                    </descriptorRefs>
091                </configuration>
092            </plugin>
093        </plugins>
094         <!-- Build with Java 1.5 -->
095        <pluginManagement>
096             <plugins>
097                <plugin>
098                    <groupId>org.apache.maven.plugins</groupId>
099                    <artifactId>maven-compiler-plugin</artifactId>
100                    <configuration>
101                        <source>1.5</source>
102                        <target>1.5</target>
103                    </configuration>
104                </plugin>
105            </plugins>
106        </pluginManagement>
107     </build>
108 
109</project>

The only items of interest in the POM are:

  • We depend on the CXF v2.1.2 client libraries and the (most excellent) SLF4J logging system
  • We invoke the cxf-codegen-plugin to run wsdl2java to generate our Java stub code into ${basedir}/target/generated/src/main/java
  • We use the build-helper-maven-plugin so that Maven can compile from two source directories (normally Maven just compiles what's in ${basedir}/src/main and not ${basedir}/target/generated/src, so we tell Maven to go compile the generated stub code too)
  • We use the maven-assembly-plugin to create a final JAR containing all necessary dependencies, which Maven will create as weather-client-jar-with-dependencies.jar when we perform a mvn assembly:assembly

At this point, even though I had no code in the project, I ran mvn assembly:assembly to build the Java stubs from the WSDL file. The output is in the generated source directory mentioned earlier, in case you want to go poke at it.

The Client Code

Once we have the autogenerated stubs we can use them in a real Java program. Before you can use the stubs, you have to identify what the actual service object is. You can find out by looking at the generated stub code and see which Java class extends Service. That will be the service interface that you call in your client. In this case, the service is called simply "Weather".

Without further ado, here's the code I wrote to invoke the SOAP service:

weather-client/src/main/java/com/logicsector/soapclient/SoapClient.java

01package com.logicsector.soapclient;
02 
03import java.text.SimpleDateFormat;
04import java.util.Date;
05import java.util.List;
06 
07 import org.slf4j.Logger;
08 import org.slf4j.LoggerFactory;
09  
10import com.cdyne.ws.weatherws.Forecast;
11import com.cdyne.ws.weatherws.ForecastReturn;
12import com.cdyne.ws.weatherws.POP;
13import com.cdyne.ws.weatherws.Temp;
14import com.cdyne.ws.weatherws.Weather;
15import com.cdyne.ws.weatherws.WeatherSoap;
16 
17 public class SoapClient {
18    private static final Logger           LOGGER      = LoggerFactory.getLogger(SoapClient.class);
19    private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("EEEE, MMMM d yyyy");
20 
21    public static void main(String[] args) {
22        try {
23            LOGGER.debug("Creating weather service instance (Note: Weather = Service subclass)...");
24             long start = new Date().getTime();
25            // Get a reference to the SOAP service interface.
26            Weather weatherService = new Weather();
27            WeatherSoap weatherSoap = weatherService.getWeatherSoap();
28            // An alternate way to get the SOAP service interface; includes logging interceptors.
29            // JaxWsProxyFactoryBean factory = new org.apache.cxf.jaxws.JaxWsProxyFactoryBean();
30            // factory.setServiceClass(WeatherSoap.class);
31            // factory.setAddress("http://ws.cdyne.com/WeatherWS/Weather.asmx");
32            // factory.getInInterceptors().add(new org.apache.cxf.interceptor.LoggingInInterceptor());
33            // factory.getOutInterceptors().add(new org.apache.cxf.interceptor.LoggingOutInterceptor());
34            // WeatherSoap weatherSoap = (WeatherSoap) factory.create();
35            long end = new Date().getTime();
36            LOGGER.debug("...Done! weatherService instance: {}", weatherService);
37            LOGGER.debug("Time required to initialize weather service interface: {} seconds", (end - start) / 1000f);
38 
39            // Send a SOAP weather request for zip code 94025 (Menlo Park, CA, USA).
40            LOGGER.debug("weatherSoap instance: {}", weatherSoap);
41            start = new Date().getTime();
42            ForecastReturn forecastReturn = weatherSoap.getCityForecastByZIP("94025");
43             end = new Date().getTime();
44            LOGGER.debug("Time required to invoke 'getCityForecastByZIP': {} seconds", (end - start) / 1000f);
45            LOGGER.debug("forecastReturn: {}", forecastReturn);
46            LOGGER.debug("forecastReturn city: {}", forecastReturn.getCity());
47            LOGGER.debug("forecastReturn state: {}", forecastReturn.getState());
48            LOGGER.debug("forecastReturn result: {}", forecastReturn.getForecastResult());
49            LOGGER.debug("forecastReturn response text: {}", forecastReturn.getResponseText());
50            LOGGER.debug("");
51            List<Forecast> forecasts = forecastReturn.getForecastResult().getForecast();
52            for (Forecast forecast : forecasts) {
53                LOGGER.debug("  forecast date: {}", DATE_FORMAT.format(forecast.getDate().toGregorianCalendar().getTime()));
54                LOGGER.debug("  forecast description: {}", forecast.getDesciption());
55                Temp temps = forecast.getTemperatures();
56                LOGGER.debug("  forecast temperature high: {}", temps.getDaytimeHigh());
57                LOGGER.debug("  forecast temperature low: {}", temps.getMorningLow());
58                POP pop = forecast.getProbabilityOfPrecipiation();
59                LOGGER.debug("  forecast precipitation day: {}%", pop.getDaytime());
60                LOGGER.debug("  forecast precipitation night: {}%", pop.getNighttime());
61                LOGGER.debug("");
62            }
63            LOGGER.debug("Program complete, exiting");
64        }
65        catch (Exception e) {
66            LOGGER.error("An exception occurred, exiting", e);
67        }
68    }
69 
70 }

Note that we're importing the stubs as import com.cdyne.ws.weatherws.Forecast, etc, within the client program. The client is also hard-coded to get the weather report from the 94025 zip code, although you could easily alter the client to take the zip code as a command-line argument.

The All-Important CXF Client Configuration File

This is the part of the development process that threw me for a loop. I didn't see any CXF documentation that indicated a cxf.xml file needs to be in the classpath of the client, so I hadn't included one in the project. My client program kept failing with a (very cryptic, very unhelpful) CXF BusException (the complete message was org.apache.cxf.BusException: No binding factory for namespace http://schemas.xmlsoap.org/soap/ registered, which I'm mentioning here in case anyone else is Googling with the same problem).

Sure, there are plenty of CXF tutorials on the Internet, but they mostly seem to build a client and a service in the same project (sharing a cxf.xml file) and I had assumed the configuration file was for configuring only the server. Silly me.

It took me several days of Googling, trying different JAR dependencies, Googling again, testing various source code modifications, Googling some more, asking for help on the cxf-user mail list — all to no avail.

Eventually, while reading the solution to an unrelated problem, I finally discovered the cause. On start-up for a server OR A CLIENT, the CXF system looks for a cxf.xml file, and fails without it. Just for the record, the BusException message is incredibly unhelpful. Grrr!! I think it should read something like org.apache.cxf.BusException: No binding factory for namespace http://schemas.xmlsoap.org/soap/ registered (did you include a cxf.xml file somewhere in the classpath?), or some such.

Anyhoo, here's the CXF configuration file I used. Not much to it. It's basically just a trivial Spring configuration with three lines of imports.

weather-client/src/main/resources/cxf.xml

04 
06 
08  
10 
11    <import resource="classpath:META-INF/cxf/cxf.xml"/>
12    <import resource="classpath:META-INF/cxf/cxf-extension-soap.xml"/>
13    <import resource="classpath:META-INF/cxf/cxf-servlet.xml"/>
14 
15 </beans>

Good thing this is easier than Axis.

Logging Configuration

For completeness, I've included the logging file I used. Since we're using LOG4J as the logging layer under SLF4J, we need to supply a log4j.properties file.

weather-client/src/main/resources/log4j.properties

1log4j.rootCategory=WARN, CONSOLE
2log4j.appender.CONSOLE=org.apache.log4j.ConsoleAppender
3log4j.appender.CONSOLE.layout=org.apache.log4j.PatternLayout
4log4j.appender.CONSOLE.layout.ConversionPattern=[%d{ABSOLUTE} %-5p %c{1}]: %m%n
5log4j.logger.com.logicsector=DEBUG

Building and Running the Client

Once you have all the code in order, it's time to build it. From within the weather-client directory, just use the previously-mentioned Maven build command to create a JAR file with dependencies:

mvn assembly:assembly

Now we can run the client. As mentioned previously, the client is hard-coded to get the weather report for the 94025 zip code (Menlo Park, California). From within the weather-client directory, the following command will start the client and invoke the service:

java -cp target/weather-client-jar-with-dependencies.jar com.logicsector.soapclient.SoapClient

If everything went smoothly you should see output something like this:

01[17:31:06,278 DEBUG SoapClient]: Creating weather service instance (Note: Weather = Service subclass)...
02Oct 18, 2008 5:31:08 PM org.apache.cxf.service.factory.ReflectionServiceFactoryBean buildServiceFromWSDL
03INFO: Creating Service {http://ws.cdyne.com/WeatherWS/}Weather from WSDL: file:/c:/Projects/weather-client/src/main/wsdl/weat
04her.wsdl
05[17:31:08,325 DEBUG SoapClient]: ...Done! weatherService instance: com.cdyne.ws.weatherws.Weather@754fc
06 [17:31:08,325 DEBUG SoapClient]: Time required to initialize weather service interface: 2.047 seconds
07[17:31:08,325 DEBUG SoapClient]: weatherSoap instance: org.apache.cxf.jaxws.JaxWsClientProxy@6458a6
08 [17:31:08,825 DEBUG SoapClient]: Time required to invoke 'getCityForecastByZIP': 0.5 seconds
09[17:31:08,825 DEBUG SoapClient]: forecastReturn: com.cdyne.ws.weatherws.ForecastReturn@aea710
10 [17:31:08,825 DEBUG SoapClient]: forecastReturn city: Menlo Park
11[17:31:08,825 DEBUG SoapClient]: forecastReturn state: CA
12[17:31:08,825 DEBUG SoapClient]: forecastReturn result: com.cdyne.ws.weatherws.ArrayOfForecast@5a2eaa
13 [17:31:08,825 DEBUG SoapClient]: forecastReturn response text: City Found
14[17:31:08,825 DEBUG SoapClient]:
15[17:31:08,825 DEBUG SoapClient]:   forecast date: Friday, October 17 2008
16 [17:31:08,825 DEBUG SoapClient]:   forecast description: Sunny
17[17:31:08,825 DEBUG SoapClient]:   forecast temperature high: 82
18[17:31:08,825 DEBUG SoapClient]:   forecast temperature low: 58
19[17:31:08,825 DEBUG SoapClient]:   forecast precipitation day: 00%
20 [17:31:08,825 DEBUG SoapClient]:   forecast precipitation night: 00%
21 [17:31:08,825 DEBUG SoapClient]:
22[17:31:08,825 DEBUG SoapClient]:   forecast date: Saturday, October 18 2008
23 [17:31:08,825 DEBUG SoapClient]:   forecast description: Sunny
24[17:31:08,841 DEBUG SoapClient]:   forecast temperature high: 73
25[17:31:08,841 DEBUG SoapClient]:   forecast temperature low: 55
26[17:31:08,841 DEBUG SoapClient]:   forecast precipitation day: 00%
27 [17:31:08,841 DEBUG SoapClient]:   forecast precipitation night: 00%
28 [17:31:08,841 DEBUG SoapClient]:
29[17:31:08,841 DEBUG SoapClient]:   forecast date: Sunday, October 19 2008
30 [17:31:08,841 DEBUG SoapClient]:   forecast description: Partly Cloudy
31[17:31:08,841 DEBUG SoapClient]:   forecast temperature high: 70
32[17:31:08,841 DEBUG SoapClient]:   forecast temperature low: 55
33[17:31:08,841 DEBUG SoapClient]:   forecast precipitation day: 00%
34 [17:31:08,841 DEBUG SoapClient]:   forecast precipitation night: 00%
35 [17:31:08,841 DEBUG SoapClient]:
36[17:31:08,841 DEBUG SoapClient]:   forecast date: Monday, October 20 2008
37 [17:31:08,841 DEBUG SoapClient]:   forecast description: Partly Cloudy
38[17:31:08,841 DEBUG SoapClient]:   forecast temperature high: 70
39[17:31:08,841 DEBUG SoapClient]:   forecast temperature low: 53
40[17:31:08,841 DEBUG SoapClient]:   forecast precipitation day: 00%
41 [17:31:08,841 DEBUG SoapClient]:   forecast precipitation night: 00%
42 [17:31:08,841 DEBUG SoapClient]:
43[17:31:08,841 DEBUG SoapClient]:   forecast date: Tuesday, October 21 2008
44 [17:31:08,856 DEBUG SoapClient]:   forecast description: Sunny
45[17:31:08,856 DEBUG SoapClient]:   forecast temperature high: 73
46[17:31:08,856 DEBUG SoapClient]:   forecast temperature low: 54
47[17:31:08,856 DEBUG SoapClient]:   forecast precipitation day: 00%
48 [17:31:08,856 DEBUG SoapClient]:   forecast precipitation night: 10%
49 [17:31:08,856 DEBUG SoapClient]:
50[17:31:08,856 DEBUG SoapClient]:   forecast date: Wednesday, October 22 2008
51 [17:31:08,856 DEBUG SoapClient]:   forecast description: Sunny
52[17:31:08,856 DEBUG SoapClient]:   forecast temperature high: 74
53[17:31:08,856 DEBUG SoapClient]:   forecast temperature low: 55
54[17:31:08,856 DEBUG SoapClient]:   forecast precipitation day: 00%
55 [17:31:08,856 DEBUG SoapClient]:   forecast precipitation night: 00%
56 [17:31:08,856 DEBUG SoapClient]:
57[17:31:08,856 DEBUG SoapClient]:   forecast date: Thursday, October 23 2008
58 [17:31:08,856 DEBUG SoapClient]:   forecast description: Sunny
59[17:31:08,856 DEBUG SoapClient]:   forecast temperature high: 73
60[17:31:08,856 DEBUG SoapClient]:   forecast temperature low: 55
61[17:31:08,856 DEBUG SoapClient]:   forecast precipitation day: 00%
62 [17:31:08,856 DEBUG SoapClient]:   forecast precipitation night: 00%
63 [17:31:08,856 DEBUG SoapClient]:
64[17:31:08,856 DEBUG SoapClient]: Program complete, exiting

Interestingly, it takes 2 seconds on my machine to initialize the interface, which seems like a really long time. CXF is probably doing a lot of stuff under the covers, but still, 2 seconds is forever in computer time.

The call to the weather service interface, once initialized, takes about half a second every time, which includes marshalling a SOAP request, sending it over the internet, receiving the response, and unmarshalling its contents. Not too bad I guess.

Concluding Thoughts

Hopefully this example will form the basis of your next SOAP client. It really is pretty easy once you see a complete and working example.

If you were ambitious, parts of this code could easily be incorporated into a web application that provides a weather report for the user. You'd simply create a servlet that takes the zip code as a parameter, invokes the SOAP service, and shows the weather report in the response. In other words, the technique of calling the SOAP service would be the same even if this was a web application.

Well, that's the end of my post about creating a CXF client with Maven. I'd love to read your comments if you found this post helpful.

Thursday, June 3, 2010

Maven|Keep Your Maven Projects Portable Throughout the Build Cycle

Keep Your Maven Projects Portable Throughout the Build Cycle

How portable is your Java project? How many modifications must be made to a build environment in order to produce a successful artifact? Determine your project's portability and learn how Maven can improve it.
f your software development lifecycle is anything like mine, it has several phases that each requires its own project configurations—which can make moving a project through all the different phases a challenge. For instance, your development phase may require you to connect to a local database, but your integration test environment database won't be local. And your test database will certainly differ from your production database (for your sake, I hope it does.).

Apache Maven 2 can help. It enables you to create a single portable Project Object Model (POM), which will relieve the integration tester and the deployment team from making changes to the project. In addition to providing enforcement of project structure, project dependency management, project encapsulation, built-in site generation, and simple integration with tools such as Subversion and Continuum, Maven aims to make portability as simple as possible while maintaining flexibility. To that end, Maven has two main tools to deal with build portability issues: properties and profiles. (See "Sidebar 1. From Make to Maven" for a closer look at the history of Java-build portability.)

Properties and Profiles

Maven properties are exactly like properties in Ant. They are placeholders for values, some of which are pre-populated. Four different styles of properties are available:
  1. env.X—Prefixing a variable with "env." will return the shell's environment variable. For example, ${env.PATH} contains the $path environment variable (%PATH% in Windows).
  2. project.x—A dot (.) notated path in the POM will contain the corresponding element's value. For example, <project><version>1.0</version></project> is accessible via ${project.version}.
  3. System Properties—All properties accessible via java.lang.System.getProperties() are available as POM properties, such as ${java.home}. (See "Sidebar 2. Viewing Property Values" for a shortcut to debugging your POM.)
  4. x—Set within a <properties /> element or an external file, the value may be used as ${someVar}.

Along with properties, Maven has added the concept of build profiles as a solution for making sweeping changes to the POM based on environmental factors. The common practice in Ant-based builds is to use properties that dictate how a project will build across environments. Maven simplifies this by removing such procedural approaches with a declaration: "If some profile is active, then use these settings." You can activate profiles via:

  • Activations: For example, "this profile will be activated if compiling with JDK1.4."
  • Command line: Using the argument -P <profileID>
  • Active profiles: An element in the "settings.xml" file containing an <activeProfile>profileID</activeProfile> element

For more details on profiles, I highly recommend you read the Maven site's guide.

Levels of Portability

Now that you have the basic tools under your belt, you can determine precisely on which level of portability your Java project falls. Maven can help you deal with the four common levels of Java portability and their corresponding concerns:
  • Wide
  • In-house
  • Environmental
  • Non-portable

Wide
In the Maven world, anyone may download a wide portability project's source, and then compile and install it (sans modification) to the POM or requirements beyond standard Maven. This is the highest level of portability; anything less requires extra work for those who wish to build your project. This level of portability is especially important for open source projects, which thrive on the ability for would-be contributors to easily download and install.

As you may imagine, being the highest level of portability makes it generally the most difficult to attain. It restricts your dependencies to those projects and tools that may be widely distributed according to their licenses (such as most commercial software packages). It also restricts dependencies to those pieces of software that may be distributed as Maven artifacts. For example, if you depend upon MySQL, your users will have to download and install it; this is not widely portable (only MySQL users can use your project without changing their systems). Using HSQLDB, on the other hand, is available from Maven Central repository, and thus is widely portable.

In-House
For most non-trivial software efforts, in-house portability is the best you can hope for. The majority of software projects fall under this listing, either for an open source development team or a closed-source production company. The center of this level of portability is your project's requirement that only a select few may access an internal (in-house) remote repository. In-house does not necessarily mean a specific company. A widely dispersed open-source project may require specific tools or connection access; this would be classified as in-house. (See "Sidebar 3. More About In-House Portability" for an expanded discussion of this level.)

Environmental
Maven created profiles for the environmental portability level, which largely concerns code and resource generation. As an example, consider a test environment that points to a separate database from that of production. When built on a test box, therefore, at least one resource file (or code—hopefully not) must be manipulated. If a file must be altered to successfully build and run on a specific environment, then the project is at best environmentally portable. A project that contains a reference to a test database in a test environment, for example, and a production database in a production environment, is environmentally portable. When you move to a different environment, one that is not defined and has no profile created for it, the project will not work. Hence, it is only portable between defined environments.

The example project is environmentally portable.

Non-Portable
A non-portable project is buildable only under a specific set of circumstances and criteria (e.g., your local machine). Unless you have no plans on porting your project to any other machines—ever, it is best to avoid non-portability entirely. Non-portable is similar to environmental portability with a single definition; the project may be built in only one environment setting.

See "Sidebar 4. Where to Draw the Line?" for help on deciding on which level your project should fall.




Solving Common Portability Problems in Maven

After defining the level of portability you need, the problem becomes moving up to or above that level. If your project is self-contained and your audience can build and run it without any modifications or installations beyond standard Maven, then congratulations! Your project is widely portable; you may now stop reading. However, if your project is non-trivial then please continue.

Avoid System Scopes
In the dependency element, avoid provided and system scopes, such as:

<dependency>
<groupId>com.closedsource</groupId>
<artifactId>sometool</artifactId>
<version>1</version>
<scope>system</scope>
<systemPath>/usr/lib/sometool.jar</systemPath>
</dependency>

If the system scope is unavoidable, you should use a property for systemPath. This allows you to set/alter that property per build environment. The best method, however, is to deploy the dependency to an in-house Maven repository (shown below) and use the dependency as a standard scope. (See "Sidebar 5. Grouping Absolute Values" for an addendum to the previous statement.)

Filter is Your Friend
Do not be shy about using filters. Filters are Maven's way of externalizing property replacement (marked in the build lifecycle as "*-process" phases) into easily portable and configurable properties files. Just add a list of standard Java properties files you wish to filter on in build, like this:

<filters>
<filter>datasource.properties</filter>
</filters>

In the example project, the "datasource.properties" exists in the base build directory (${buildDir}) and contains the "name=value" pair for the "jdbc.url":


jdbc.url=jdbc:driver://localhost/myDB

When resources are filtered, the project replaces all matching property names with their corresponding values, taking the filter list into account. The resources to be filtered are defined by the "resources" build element. For example, this block says that your project has XML resources that should be filtered, and that the results will be put into the META-INF directory:


<resources>
<resource>
<filtering>true</filtering>
<directory>src/main/resources</directory>
<targetPath>META-INF</targetPath>
<includes>
<include>*.xml</include>
</includes>
</resource>
</resources>

You can run the example in the sample project by executing the "process-resources" phase in the command line. It will convert the "datasource.xml" resource file from this:


<datasource>
<jdbc-url>${jdbc.url}</jdbc-url>
</datasource>

Into this (in the "target/META-INF" directory):


<datasource>
<jdbc-url>jdbc:driver://localhost/myDB</jdbc-url>
</datasource>

Environmental Portability with Profiles

As a general rule, try to make one profile per environment. Unless you are building on more than five or so environment types, I suggest putting the profile in the POM. Then you may activate the desired profile via the -P argument on the command line. You can test which profiles are currently active with the "help" plugin, like this:

mvn help:active-profiles

You can make environmental changes even simpler by utilizing the "settings.xml" file. The example project contains three profiles: env-dev, env-test, and env-prod. If you wish to ensure that the env-test profile is always activated on your test environment, add the activeProfile to that environment's "settings.xml" file, as follows:


<settings>
...
<activeProfiles>
<activeProfile>env-test</activeProfile>
</activeProfiles>
</settings>

For every environment-specific project, name the test profile env-test. Maven will simply ignore the "activeProfile" line if it does not exist. No harm, no foul. The sample project contains a "settings.xml" file. Feel free to experiment.

Profiles, Profiles, Everywhere

What if you have many more profiles? Some projects require subtle build alterations for several clients, sometimes hundreds. The thing to do in this scenario is create "profiles.xml" files that may be copied at the necessary build time. A "profiles.xml" file is exactly like a "pom.xml" profiles element, containing one or more profile elements. It is merely externalized. For example, the profile for your first client may be:

<profiles>
<profile>
<id>client-0001</id>
<activation>
<activeByDefault>true</activeByDefault>
</activation>
<properties>
<!-- The client loves blue -->
<css.pref>blue.css</css.pref>
<!-- They want standard pkg -->
<module>default</module>
</properties>
</profile>
</profiles>

Create one profile per "profiles.xml" and make it activeByDefault. Its very existence will ensure that the profile is utilized in the build. If the above "profiles.xml" file is in the base build directory, the help:active-profiles goal will print this:


The following profiles are active:
- client-0001 (source: profiles.xml)

Use Profiles Not Properties

(This advice is directed toward in-house projects with fixed environments, not a widely portable open source or plugin project.)

A minor task that I often insist upon is creating environment profiles rather than passing in command-line properties, even if the profile consists of only one property. The logic behind this is simple. If you created the following profile for an environment:


<profiles>
<profile>
<id>env-test</id>
<properties>
<install.location>/testbox/app</install.location>
</properties>
</profile>
</profiles>

You command-line activation would be this:


mvn install -P env-test

instead of this:


mvn install -Dinstall.location=/user/local/test

Now imagine that you need to add another property. The manual property settings will become longer, while the profiled POM command-line remains fixed, regardless of the number of properties you may add in the future:


mvn install -Dinstall.location=/user/local/test -Dnew.prop=true

This becomes important if you decide to use a continuous integration server like Continuum.




Avoid Project Structure Funkiness

Injecting a lot of Ant code via the "maven-antrun-plugin" in your POM is a leitmotif of non-portability. If you find that other Maven plugins routinely fall short of your requirements, it is smart to look first at your project structure.

This leads me into another nefarious piece of Maven non-portability: project nesting. For example, nesting WARs in EARs like so:


myEar
pom.xml
META-INF/application.xml
myWar
src/MyServlet.java
WEB-INF/web.xml

Before Maven, this was a common setup for projects that consisted of a WAR with a single EAR. Ant—enforcing no structure at all—happily obliged. Sometimes people attempt to port structures like this to Maven and build projects via embedded Ant or with the "maven-assembly-plugin". Say you write such a project for a single customer. Then, along comes a new customer who requires a different EAR configuration. Your WAR is logically a separate project, but porting it into another EAR proves quite difficult. Your ability to port this project to a new customer is now limited.

This is an example of attempting to force Maven non-standard project structures into the Maven framework. In this particular example, you should split the EAR and WAR into separate projects, like this:


myEar
pom.xml
src/main/resources/META-INF/application.xml
myWar
pom.xml
src/main
java/MyServlet.java
resources/WEB-INF/web.xml

Have a new client with a new EAR? No problem, just add a new EAR project with a dependency on myWar. Don't worry; you'll learn to love it.

Create Your Own Public Repository

If your project has dependencies that exist on a repository other than Maven-central, consider creating a public repository of your own. This will require a public Web server, some mechanism to deploy files to that Web server's filesystem, such as an FTP server, and a few changes to your POMs. Many articles on the Web explain how to install and set up your transport mechanism of choice, but this example uses WebDAV to accept project uploads. After your servers are configured, create a parent POM from which all of your projects may inherit. This makes deployment much simpler. Add the following to a parent POM:

<project>
<artifactId>my-parent</artifactId>
...
<build>
<extensions>
<extension>
<groupId>org.apache.maven.wagon</groupId>
<artifactId>wagon-webdav</artifactId>
<version>1.0-beta-1</version>
</extension>
</extensions>
</build>
<distributionManagement>
<repository>
<id>codehaus-mojo</id>
<name>Repository Name</name>
<url>dav:https://dav.codehaus.org/repository/mojo/</url>
</repository>
</distributionManagement>
</project>

The URL prefix corresponds to one of the supported Wagon providers, the mechanism that Maven uses to transport your files to the correct repository. (Click here for the list of supported provider types.) Besides specifying the distribution management, you must add a build extension corresponding to the transport mechanism you wish to use. In this case, WebDAV, so I added "wagon-webdav".

In order to control who may deploy their builds to this server, you will probably assign your core developers usernames. Each user can set up his or her settings.xml files (under their Maven install conf or local repository directories) to the matching repository ID, as follows:


<settings>
...
<servers>
<server>
<id>codehaus-mojo</id>
<username>joe</username>
<password>c4ntGuessThi5</password>
</server>
</servers>
...
</settings>

Requiring a change to your developer's local setup in this way does not really affect portability. You are concerned with only your client builder's ability to build and install without making local changes. That does not mean you want to make it easy for them to deploy to your repository.

Now for any project that depends upon other deployed projects, you can add the public face of your repository (made public by a simple Web server, such as Apache HTTP Server) via the repository element, like this:


<project>
...
<repositories>
<repository>
<id>codehaus-mojo</id>
<url>http://repository.codehaus.org/org/codehaus/mojo/</url>
</repository>
</repositories>
</project>

Your project is now widely portable. If you wish to be merely in-house portable, you can restrict network access, effectively creating an exclusive club for your repository (all you need is the secret handshake).




In-House, Commercial-License Remote Repository

Sometimes licensing issues prevent projects from being as portable as they could be, forcing your teams to manually download and install the artifact. Normally, this would earn your project a spot in the non-portable category, but the Maven team has reduced this issue to only a minor annoyance. Say that you licensed a closed-source JAR named "sometool.jar". You could install it within an in-house repository as follows, allowing other coworkers to access the JAR just like any other Maven artifact:

mvn deploy:deploy-file -DgroupId=com.closedsource -DartifactId=sometool -Dversion=1.0 -Dpackaging=jar
-Dfile=sometool.jar -DgeneratePom=true -Durl=scp://inhouse/maven -DlocalRepository=inhouse

Viola! Your non-portable project is now portable in-house, subject to licensing restrictions.

Getting javax.*

All javax.* packages once required you to manually download and install the artifacts from the Sun Web site due to licensing restrictions. This is no longer the case, as java.net has created public Maven 1.x and 2.x repositories containing some packages, such as javax.mail and javax.persistence. You may access both repositories with the following added to your project's POM (notice the layout element in the Maven 1.x repository):

<repository>
<id>java.net</id>
<url>https://maven-repository.dev.java.net/nonav/repository</url>
<layout>legacy</layout>
</repository>
<repository>
<id>maven2-repository.dev.java.net</id>
<name>Java.net Repository for Maven</name>
<url>https://maven2-repository.dev.java.net/nonav/repository</url>
</repository>

Your once non-portable project using the email API is now widely portable, with no manual installs required.

The Dream of Portability

Thanks to Maven, you can begin standardizing the build process itself. While not perfect, Maven is taking a giant leap forward for Java development, making the dream of pure, simple portability a reality.

Eric Redmond has worked as build architect of multi-million-dollar (and million-line) Java EE projects, but currently works as the senior engineer of a four-man team. In his spare time, he enjoys combining Java and Ruby in unholy ways.