Using Maven profiles and resource filtering
Posted by Paolo Predonzani
on June 23, 2009
Introduction
In this tutorial I'll explain how to deal with multiple deployment environments (such as development and production) from a configuration perspective. Specifically I'll show how environment-specific configurations, such as database connections and resource contents, can be "managed" in a centralized way by the developer.
This tutorial assumes some working knowledge of Maven overlays. The Using Maven overlays to build customized applications tutorial is a good starting point, which also introduces the Portofino example that we'll expand and refine here.
If you're impatient (and a bit of a Maven expert) you can jump to the section "Putting it all together: the pom.xml file", where the techniques described in this tutorial are summarized in a single POM file.
Contents
Definition and purpose
Maven profiles allow a parametrization of the build process with the purpose of making the target application portable across several deployment environments. A profile is the centralized place where you can define build parameters that apply only to certain environments but not to others.
In our case the database connection details are environment-specific. We'll have a "development" profile customized to the needs of a development environment (webapp and database running on "localhost", e.g., the developer's personal laptop) and a "production" profile customized to the needs of a production environment (webapp pointing to a certain database server).
Also we want the application logo to be different for the two environments.
Profiles can be activated automatically, e.g., by detecting a certain operating system, jdk version, etc., but can also be activated manually by the developer during the build process. We'll work with the latter situation.
Profiles can be specified in the pom.xml
file or in a separate profile.xml
file at the top level of the project structure. They can also be specified in user-specific files, outside the project directory structure. More details can be found in Maven's official introduction to build profiles.
For simplicity in our examples we'll define profiles inside the main pom.xml
, along with other plugin configurations.
Example application
Throughout this tutorial we'll use the same example application we used in the previous Maven overlays tutorial. Briefly, this is a war-overlay project built on top of a standard Portofino distribution. The project uses Maven to introduce some basic configuration and customizations.
Let's have a look at the files:
$ find . . ./pom.xml ./src ./src/main ./src/main/resources ./src/main/resources/portofino-custom.properties ./src/main/webapp ./src/main/webapp/mylogo.png ./src/main/webapp/mywebapp.css ./src/main/webapp/WEB-INF
Not much, but enough for our goals. The relevant files are:
-
pom.xml
: the Maven project file -
portofino-custom.properties
: Portofino's configuration file -
mylogo.png
: a custom logo image -
mywebapp.css
: a custom css stylesheet
So this is our starting point. Let's apply the profiles to it.
The "profiles" section
A "profiles" section in the pom.xml file can contain one or more profile definitions.
To define two profiles called "development" and "production" simply enter the following fragment:
<profiles> <profile> <id>development</id> <!-- we'll add more stuff here... --> </profile> <profile> <id>production</id> <!-- ...and here --> </profile> </profiles>
Now we want to associate some parameters to each profile. In our case there will be:
- four parameters that provide the db connection details to a Portofino application;
- one parameter to set the application logo.
The fragment for the development environment must be modified as follows:
<profile> <id>development</id> <properties> <db.driverClass>oracle.jdbc.driver.OracleDriver</db.driverClass> <db.connectionURL>jdbc:oracle:thin:@127.0.0.1:1521:XE</db.connectionURL> <db.username>devuser</db.username> <db.password>devpassword</db.password> <logo.image>mylogo.png</logo.image> </properties> </profile>
Notice the five properties db.driverClass, db.connectionURL, db.username, db.password, and logo.image. You can choose any name for them - there is no particular convention.
Also notice that, with the shown values, we intend to connect to an Oracle XE instance running on the local machine.
For production, the fragment becomes:
<profile> <id>production</id> <properties> <db.driverClass>oracle.jdbc.driver.OracleDriver</db.driverClass> <db.connectionURL>jdbc:oracle:thin:@10.0.1.14:1521:APPS</db.connectionURL> <db.username>productionuser</db.username> <db.password>productionpassword</db.password> <logo.image>production_logo.png</logo.image> </properties> </profile>
This profile uses the Oracle driver too, but points to a different database server and instance.
All the properties and values we've seen so far are just declared but not used yet. To get something useful out of them, we need to dig into (web) resource filtering, as discussed in the following two sections.
Resource filtering
Resource filtering is about performing an automatic "find & replace" on property files and other classpath resources used in a project. Maven's resources plugin takes care of filtering and must be configured like this:
<plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-resources-plugin</artifactId> <version>2.3</version> <configuration> <!-- specify UTF-8, ISO-8859-1 or any other file encoding --> <encoding>UTF-8</encoding> </configuration> </plugin>
Also you must tell the plugin where the resources are located and that filtering must be switched on. These lines also go into the pom.xml:
<resources> <resource> <directory>src/main/resources</directory> <filtering>true</filtering> </resource> </resources>
We have set up everything in the pom.xml file. Now let's look at the resources that will actually be filtered.
The portofino-custom.properties
file in src/main/resources
is just the resource we need. This is Portofino's main configuration file, containing the database connection details. The original file looked like this:
database.jdbc.driverClass=oracle.jdbc.driver.OracleDriver database.jdbc.connectionURL=jdbc:oracle:thin:@127.0.0.1:1521:XE database.jdbc.username=myusername database.jdbc.password=mypassword model.application.name=MyWebApp model.stylesheet=/mywebapp.css
Let's modify it to take advantage of resource filtering:
database.jdbc.driverClass=${db.driverClass} database.jdbc.connectionURL=${db.connectionURL} database.jdbc.username=${db.username} database.jdbc.password=${db.password} model.application.name=MyWebApp model.stylesheet=/mywebapp.css
Notice that db.driverClass, db.connectionURL, etc are exactly the properties we've defined in the profiles, wrapped inside the ${...} notation that instructs the resource plugin to perform a substitution of values.
Some security considerations
With the approach we've seen, database connection passwords are stored in pom.xml
. This file, most likely, will end up in the version control system and shared by a group of people. This may be good or bad from a security perspective.
It can be good because the location of the passwords is known and declared upfront. Most version control systems can be configured to grant access to the project files only to the people who really need to work on them. So this approach is structured, has tool support, and only requires the project team to trust its members.
However it can also be bad, if company policies do not allow passwords to be stored in the source files or if the development team and the deployment team must not share knowledge of passwords. In these situations, you may want to investigate "identd" authentication techniques, which don't require passwords and basically authenticate the connection based on the user (in a unix or windows domain sense) running the application server's process.
Webapp resource filtering
A web application resource is any jsp, html, css, javascript, etc. resources located in the src/main/webapp
directory of a Maven project. These are different from the resources we discussed in the "Resource filtering" section, which end up in the WEB-INF/classes
directory of the WAR file.
Many Maven users find this distinction confusing at first, thinking that all resources are of the same type. However, in Maven, webapp resources are treated differently, as explained in the the official maven-war-plugin documentation.
Another way to think about this distinction is classpath resources vs. non-classpath resources. Webapp resources are of the latter type.
The maven-war-plugin is responsible for webapp resource filtering. To configure it use the following pom.xml fragment:
<plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-war-plugin</artifactId> <version>2.0.2</version> <configuration> <webResources> <resource> <filtering>true</filtering> <directory>src/main/webapp</directory> <includes> <include>**/*.css</include> <!-- include any other file types you want to filter --> </includes> </resource> </webResources> </configuration> </plugin>
Notice that we've set up the plugin to filter .css files.
The only interesting webapp resource we have is src/main/webapp/mywebapp.css
, a stylesheet that looks like this:
@import url("default.css"); div#logo a { /* some lines omitted */ background: url('mylogo.png') no-repeat 0px 0px; /* some more lines omitted */ }
Our goal is to parametrize the logo image. Edit the file:
@import url("default.css"); div#logo a { /* some lines omitted */ background: url('${logo.image}') no-repeat 0px 0px; /* some more lines omitted */ }
The logo.image property allows the application to point to different logo images depending on the profile that is selected:
- 'mylogo.png' for development
- 'production_logo.png' for production
Of course make sure you add a 'production_logo.png' to the project.
Putting it all together: the pom.xml file
Putting all the fragments we've discussed in the previous sections we finally get the following pom.xml:
<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/maven-v4_0_0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.example</groupId> <artifactId>mywebapp</artifactId> <packaging>war</packaging> <version>1.0-SNAPSHOT</version> <name>mywebapp Maven Webapp</name> <url>http://www.example.com</url> <dependencies> <!-- The following dependency makes this project a war overlay --> <dependency> <groupId>com.manydesigns</groupId> <artifactId>portofino-war</artifactId> <version>3.0</version> <type>war</type> <scope>compile</scope> </dependency> <!-- Other dependencies go here --> </dependencies> <build> <finalName>mywebapp</finalName> <plugins> <!-- Enabling and configuring regular resources filtering. See also section "resources" below --> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-resources-plugin</artifactId> <version>2.3</version> <configuration> <!-- specify UTF-8, ISO-8859-1 or any other file encoding --> <encoding>UTF-8</encoding> </configuration> </plugin> <!-- Enabling and configuring web resources filtering --> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-war-plugin</artifactId> <version>2.0.2</version> <configuration> <webResources> <resource> <filtering>true</filtering> <directory>src/main/webapp</directory> <includes> <include>**/*.css</include> <!-- include any other file types you want to filter --> </includes> </resource> </webResources> </configuration> </plugin> </plugins> <!-- Instructing the resources plugin to filter certain directories --> <resources> <resource> <directory>src/main/resources</directory> <filtering>true</filtering> </resource> </resources> </build> <!-- Profiles start here --> <profiles> <!-- Development environment @ my laptop --> <profile> <id>development</id> <properties> <db.driverClass>oracle.jdbc.driver.OracleDriver</db.driverClass> <db.connectionURL>jdbc:oracle:thin:@127.0.0.1:1521:XE</db.connectionURL> <db.username>devuser</db.username> <db.password>devpassword</db.password> <logo.image>mylogo.png</logo.image> </properties> </profile> <!-- Production environment @ production server --> <profile> <id>production</id> <properties> <db.driverClass>oracle.jdbc.driver.OracleDriver</db.driverClass> <db.connectionURL>jdbc:oracle:thin:@10.0.1.14:1521:APPS</db.connectionURL> <db.username>productionuser</db.username> <db.password>productionpassword</db.password> <logo.image>production_logo.png</logo.image> </properties> </profile> </profiles> </project>
Building the target
Ok, we're ready to build. Let's check we have all the files in place.
$ find . . ./pom.xml ./src ./src/main ./src/main/resources ./src/main/resources/portofino-custom.properties ./src/main/webapp ./src/main/webapp/mylogo.png ./src/main/webapp/mywebapp.css ./src/main/webapp/production_logo.png ./src/main/webapp/WEB-INF
Then run:
mvn package -P development
Make sure you get the "BUILD SUCCESSFUL" message.
This runs the target using the development profile (notice the "-P" parameter). Maven creates the WAR archive target/mywebapp.war
and an expanded version under target/mywebapp/
.
Let's check that resource filtering has actually been applied.
$ cat target/mywebapp/WEB-INF/classes/portofino-custom.properties database.jdbc.driverClass=oracle.jdbc.driver.OracleDriver database.jdbc.connectionURL=jdbc:oracle:thin:@127.0.0.1:1521:XE database.jdbc.username=devuser database.jdbc.password=devpassword model.application.name=MyWebApp model.stylesheet=/mywebapp.css
And:
$ cat target/mywebapp/mywebapp.css @import url("default.css"); div#logo a { display: block; position: absolute; top: 14px; /* distance from top margin */ left: 20px; /* distance from left margin */ text-decoration: none; color: black; font-weight: bold; font-size: 2em; background: url('mylogo.png') no-repeat 0px 0px; padding-top: 60px; /* the logo's height */ height: 0px; width: 200px; /* the logo's width */ overflow: hidden; }
The words in bold show the applied substitutions.
If you want to build for the production environment you can run:
mvn package -P production
And verify that another set of substitutions have been applied.
Further readings
Maven can be employed in many useful situations. For a real life application see our tutorial Integrating Portofino with Alfresco.