Transitioning to the Target Platform...

So how does one take existing projects and have them leverage the Liferay target platform BOMs?

Introduction

So I'm not sure if you've had the chance to play around with the new Target Platform stuff that Liferay has, but it is really cool.

If you haven't read about it or don't know what it is for, let me just summarize quickly:

You declare a "dependency" on a Liferay-shipped BOM file, the "bill of materials" file for a project that lists all of the dependencies and versions that Liferay ships in a specific release.

You remove all explicitly declared versions from your own dependency lists.

When you build your artifact, you will have specific version dependencies but those values will come from the BOM, rather than from your own list.

Why is this kind of thing valuable? Well, if you have created a new Liferay project and found yourself trying to track down what version of a specific jar was released in your target production environment so you could list that version as your dependency, you quickly will realize what a pain this can be.

I mean, like I have a project which I want to target to 7.0 GA5 (because my production environment hasn't been updated yet even though my dev environment is at 7.0 GA7), and I have the following dependencies listed in my build.gradle file:

dependencies {
  compileOnly group: "com.liferay", name: "com.liferay.application.list.api", version: "2.0.0"
  compileOnly group: "com.liferay.portal", name: "com.liferay.portal.kernel", version: "2.16.0"
  compileOnly group: "javax.portlet", name: "portlet-api", version: "2.0"
  compileOnly group: "javax.servlet", name: "javax.servlet-api", version: "3.0.1"
  compileOnly group: "jstl", name: "jstl", version: "1.2"
  compileOnly group: "org.osgi", name: "org.osgi.service.component.annotations", version: "1.3.0"
  compileOnly group: "com.liferay", name: "com.liferay.frontend.taglib", version: "2.0.0"
  compileOnly group: "com.liferay.portal", name: "com.liferay.util.taglib", version: "2.0.0"
}

So I honestly don't know what the versions of each of the com.liferay dependencies I really need. I took a guess at the com.liferay.portal.kernel one and the others are, well, just 2.0.0 because I wasn't using anything newer.

So my portlet really doesn't target any environment specifically. Maybe that's okay, but what happens when my boss asks "Hey, can you build that thing and deploy it to 7.1?" In order to find out, I could either take  a deep dive into a 7.1 bundle to see what versions are there, I could check the portal source for 7.1 to see if I can find what versions they think they are going to be, or maybe I go rooting around in Maven Central or Liferay's public repository to see what version(s) are listed in there.

I could even pick versions at random until I got a combination that maybe builds and deploys, but boy what a pain this is.

Well, that's where the Target Platform stuff is really going to help you out.

Using the Target Platform settings in your new projects as defined in Managing the Target Platform for Liferay Workspace, you specify the Liferay version you're pointing at and strip out the versions of your dependencies, then you let Gradle figure out what versions you can use.

In this case, my build.gradle file can then be reduced to:

dependencies {
  compileOnly group: "com.liferay", name: "com.liferay.application.list.api"
  compileOnly group: "com.liferay.portal", name: "com.liferay.portal.kernel"
  compileOnly group: "javax.portlet", name: "portlet-api"
  compileOnly group: "javax.servlet", name: "javax.servlet-api"
  compileOnly group: "jstl", name: "jstl"
  compileOnly group: "org.osgi", name: "org.osgi.service.component.annotations"
  compileOnly group: "com.liferay", name: "com.liferay.frontend.taglib"
  compileOnly group: "com.liferay.portal", name: "com.liferay.util.taglib"
}

I don't have to worry about what version of com.liferay.portal.kernel I need to use, I just say my target platform is "7.0.6" and my dependency versions will all match whatever was part of the 7.0 GA5 release.

Now when my boss asks about 7.1, I just change my target platform to "7.1.0" and do a build; I'll know right away if my code can build for 7.1 (I'll still need to test it out, of course), but I don't have to spend all of those cycles tracking down individual version releases.

I can also do the same kind of thing to ensure my code compiles for 7.0 GA7 or 7.0 GA3 or whatever, just by changing the target platform and compiling.

Again, this will not necessarily guarantee your code works in the target environment, but at least you'll know you wouldn't be facing unresolved reference errors if you deploy there...

Okay, so now we know the benefits of having the target platform. And, if you've checked the doco, it is super easy to make changes when you already have the target platform stuff in place.

But what about if you're like me and you have existing projects that either haven't been wired up for target platforms or haven't been updated so target platforms is an option?

Well, that's where we're headed in this blog post...

Maven Non-Liferay-Workspace Project

So BOMs are actually a Maven thing, so it is super easy to add target platform details into these kinds of projects.

You have two options here:

  1. Add a parent project, in that project import the BOMs.
  2. Add the import scope dependencies.

With the parent project option, if you don't have a parent, you can add this near the top of your own pom file, something like:

<?xml version="1.0"?>

<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>com.dnebinger</groupId>
  <artifactId>my-portlet</artifactId>
  <version>1.0.0</version>
  <parent>
    <groupId>com.dnebinger</groupId>
    <artifactId>my-parent</artifactId>
    <version>1.0.0</version>
  </parent>

  <properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
  </properties>
  <dependencies>
    <dependency>
      <groupId>com.liferay.portal</groupId>
      <artifactId>com.liferay.portal.kernel</artifactId>
      <version>3.0.0</version>
      <scope>provided</scope>
    </dependency>
    ...

In your parent pom, you'll do the import scopes that are coming up next...

If you do have a parent already, or you are creating your parent pom, or you just don't want to add a parent project, you can add a dependency on the BOMs with the special <dependencyManagement /> section shown below. Note that you're still working with a regular POM file that has all of the regular stuff, but you're just adding the following <dependencyManagement /> tag:

<dependencyManagement>
  <dependencies>
    <dependency>
      <groupId>com.liferay.portal</groupId>
      <artifactId>release.portal.bom</artifactId>
      <version>7.1.3</version>
      <type>pom</type>
      <scope>import</scope>
    </dependency>
    <dependency>
      <groupId>com.liferay.portal</groupId>
      <artifactId>release.portal.bom.compile.only</artifactId>
      <version>7.1.3</version>
      <type>pom</type>
      <scope>import</scope>
    </dependency>
    <dependency>
      <groupId>com.liferay.portal</groupId>
      <artifactId>release.portal.bom.third.party</artifactId>
      <version>7.1.3</version>
      <type>pom</type>
      <scope>import</scope>
    </dependency>
  </dependencies>
</dependencyManagement>

So just a quick review here, ...

We're using a <dependencyManagement /> section, not just the regular <dependencies />. The <dependencyManagement/> section doesn't actually pull in the listed artifacts as actual project dependencies, but it does specify the versions that are normally to be used when resolving project dependencies.

We've declare the type for these three dependencies as a pom because, well, that's what they are.

We've used the special import scope, so this is going to pull the referenced pom into our current pom and include the reference details.

There are 3 POMs that we're including:

  • release.portal.bom - Defines the Liferay artifacts and versions in the target release.
  • release.portal.bom.compile.only - Defines the Liferay artifacts and versions which are compile-only dependencies such as the OSGi annotations jar and the BND jar, etc.
  • release.portal.bom.third.party - Defines the third party artifacts and versions in the target release. This includes things like the Apache Commons libraries, Spring, etc.

The only remaining step we need to take is to remove all of the <version /> tags from our regular listed dependencies. This will ensure the version we use is the one that comes from the target platform. My pom then looks like:

<?xml version="1.0"?>

<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>com.dnebinger</groupId>
  <artifactId>my-portlet</artifactId>
  <version>1.0.0</version>
  <properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <liferay.target.platform>7.1.3</liferay.target.platform>
  </properties>
  <dependencyManagement>
    <dependencies>
      <dependency>
        <groupId>com.liferay.portal</groupId>
        <artifactId>release.portal.bom</artifactId>
        <version>${liferay.target.platform}</version>
        <type>pom</type>
        <scope>import</scope>
      </dependency>
      <dependency>
        <groupId>com.liferay.portal</groupId>
        <artifactId>release.portal.bom.compile.only</artifactId>
        <version>${liferay.target.platform}</version>
        <type>pom</type>
        <scope>import</scope>
      </dependency>
      <dependency>
        <groupId>com.liferay.portal</groupId>
        <artifactId>release.portal.bom.third.party</artifactId>
        <version>${liferay.target.platform}</version>
        <type>pom</type>
        <scope>import</scope>
      </dependency>
    </dependencies>
  </dependencyManagement>
  <dependencies>
    <dependency>
      <groupId>com.liferay.portal</groupId>
      <artifactId>com.liferay.portal.kernel</artifactId>
      <scope>provided</scope>
    </dependency>
    <dependency>
      <groupId>com.liferay</groupId>
      <artifactId>com.liferay.portal.security.audit.storage.api</artifactId>
      <scope>provided</scope>
    </dependency>
    <dependency>
      <groupId>com.liferay</groupId>
      <artifactId>com.liferay.application.list.api</artifactId>
      <scope>provided</scope>
    </dependency>
    <dependency>
      <groupId>com.liferay</groupId>
      <artifactId>com.liferay.journal.api</artifactId>
      <scope>provided</scope>
    </dependency>
    <dependency>
      <groupId>com.liferay</groupId>
      <artifactId>com.liferay.osgi.util</artifactId>
      <scope>provided</scope>
    </dependency>
    <dependency>
      <groupId>com.liferay</groupId>
      <artifactId>com.liferay.registry.api</artifactId>
      <scope>provided</scope>
    </dependency>
    <dependency>
      <groupId>com.liferay.portal</groupId>
      <artifactId>com.liferay.util.taglib</artifactId>
      <scope>provided</scope>
    </dependency>
    <dependency>
      <groupId>javax.portlet</groupId>
      <artifactId>portlet-api</artifactId>
      <scope>provided</scope>
    </dependency>
    <dependency>
      <groupId>javax.servlet</groupId>
      <artifactId>javax.servlet-api</artifactId>
      <scope>provided</scope>
    </dependency>
    <dependency>
      <groupId>jstl</groupId>
      <artifactId>jstl</artifactId>
      <scope>provided</scope>
    </dependency>
    <dependency>
      <groupId>org.osgi</groupId>
      <artifactId>osgi.cmpn</artifactId>
      <scope>provided</scope>
    </dependency>
    <dependency>
      <groupId>org.osgi</groupId>
      <artifactId>org.osgi.core</artifactId>
      <scope>provided</scope>
    </dependency>
    <dependency>
      <groupId>commons-lang</groupId>
      <artifactId>commons-lang</artifactId>
      <scope>provided</scope>
    </dependency>
    <dependency>
      <groupId>org.slf4j</groupId>
      <artifactId>slf4j-api</artifactId>
      <scope>provided</scope>
    </dependency>
  </dependencies>
  
  [snip]
  
</project>

I've used a property for the liferay target platform (to easily change it later), I've included the <dependencyManagement /> section, and I've removed all of the <version /> tags from my dependencies.

Note that if I wanted a different version than what Liferay provides or if I'm using a dependency that Liferay doesn't provide, I'm still going to keep the <version /> tag on those dependencies so they resolve correctly from MavenCentral.

The benefit of using a parent pom is that you can share the parent across multiple actual projects, so it becomes easy to get the target platform applied and updates affect all of the child projects without changing them individually.

Maven Liferay Workspace

So whether you have new or old version of the Maven-based workspace, it really doesn't matter because currently the Maven workspace template does not include the target platform support (yet).

So the instructions in this case are really the same for the above section:

  1. Add the <dependencyManagement /> section into your root-level pom.xml file.
  2. Remove the <version /> tags from the artifacts that should get their versions from the target platform.

That's pretty much it!

Gradle Liferay Workspace

So first of all, for target platform support you will need the Liferay workspace plugin version 1.9.0 or greater.  How do you know what version you have? Check the settings.gradle file at the top for lines like:

buildscript {
  dependencies {
    classpath group: "com.liferay", name: "com.liferay.gradle.plugins.workspace", version: "1.10.9"
    classpath group: "net.saliman", name: "gradle-properties-plugin", version: "1.4.6"
  }

From this project I can see that I have 1.10.2, so my version is high enough. If you don't have 1.9.0 or newer, you can check Updating Liferay Workspace to help get beyond 1.9.0.

The rest of our steps comes from Managing the Target Platform for Liferay Workspace. First we open the gradle.properties file and either add or update the liferay.workspace.target.platform.version property to be the version we want to target.

Then, in all of our build.gradle files, we go through and strip out explicit version numbers, keeping those that we either want to specifically override from Liferay or dependencies which Liferay doesn't have a version to provide.

Now, when we build the full workspace or just some of the modules, the dependency versions will come from the target platform.

Gradle Stand-Alone Projects

This was actually the hardest one for me to get working...

There is also target platform support for Gradle stand-alone projects, but it is a little more involved to get it working.

The definitive documentation will come from the Target Platform Gradle Plugin page, but as of the moment when I publish this, it does have a few minor errors, but they have been reported and will be addressed soon.

Your build.gradle file will be including a new Liferay plugin for the Target Platform support, as well as some new configurations to handle the versions. If you are in a multi-project build, note that the changes highlighted below will only go in the build.gradle for the root project, not the subprojects.

I started with my project for external DB access in Service Builder: https://github.com/dnebing/sb-extdb

I made a couple of changes. In the root build.gradle file, I changed to:

buildscript {
  repositories {
    maven {
      url "https://repository-cdn.liferay.com/nexus/content/repositories/public/"
    }
  }
  dependencies {
    classpath group: "com.liferay", name: "com.liferay.gradle.plugins", version: "4.0.9"
    classpath group: "com.liferay", name: "com.liferay.gradle.plugins.target.platform", version: "2.0.0"
  }
}

apply plugin: "com.liferay.target.platform"

dependencies {
  targetPlatformBoms group: "com.liferay.portal", name: "release.portal.bom", version: "7.0.6"
  targetPlatformBoms group: "com.liferay.portal", name: "release.portal.bom.compile.only", version: "7.0.6"
  targetPlatformBoms group: "com.liferay.portal", name: "release.portal.bom.third.party", version: "7.0.6"
}

subprojects {
  buildscript {
    repositories {
      maven {
        url "https://repository-cdn.liferay.com/nexus/content/repositories/public/"
      }
    }
  }

  apply plugin: "com.liferay.plugin"
	
  repositories {		
    maven {
      url "https://repository-cdn.liferay.com/nexus/content/repositories/public/"
    }
  }
}

This added in the Target Platform plugin and also pulls in the BOMs for 7.0.6 for my target.

Then I needed to update my subproject build.gradle files to remove version details. Here's the one from the sb-extdb-postlogin subproject:

dependencies {
  compile  project(':sb-extdb-api')
  compile  project(':sb-extdb-service')
  compile group: "biz.aQute.bnd", name: "biz.aQute.bndlib"
  compile 'com.liferay.portal:com.liferay.portal.kernel'
  compile 'com.liferay:com.liferay.osgi.util'
  compile 'com.liferay:com.liferay.portal.spring.extender'
  compile 'commons-collections:commons-collections'
  compile 'commons-lang:commons-lang'
  compile 'javax.portlet:portlet-api'
  compile 'javax.servlet:javax.servlet-api'
  compile 'org.osgi:org.osgi.compendium:5.0.0'
  compile 'org.osgi:org.osgi.service.component.annotations'
}

After these changes, I could do the build targeting 7.0.6 but not have to know the versions that I was compiling against.

Note that both the formats are supported for dependency use, the more verbose group: "", name: "" format as well as the condensed "group:name" format.

Also note that I had to include version for the org.osgi.compendium dependency because that one is not exposed by Liferay (I really shouldn't have used it, but it made a easy way to show how to override).

Conclusion

So here we have introduced the Target Platform support into our existing projects. It should make it easier to change the target platform version without having to track down all of those version details.

I will continue to suggest that you target the oldest version you can. For example, don't just set your target platform to 7.1.3 and ignore 7.1.0, 7.1.1 and 7.1.2 unless you have a really valid reason for doing so, valid in like those versions don't have an API you need or something.

It is the best way to ensure that your code can be deployed to the widest range of Liferay versions and not limit the possibilities.

I do get in arguments with coworkers over this, but not because of limiting the deployment options. They rely on the target platform because it pulls in the sources for all of the components they're using and, when debugging, their IDE source will match the runtime source.

I don't disagree with using target platforms this way, my focus is strictly on choosing the best target for your build and deployment aspects.

Ah, well, I hope you enjoy this as much as I enjoyed figuring all of this out!

 

Blogs

Awesome stuff - I was just looking at exactly the same thing over the past few days (as I upgrade from 7.1 GA1 to GA4) and was having a few issues on dependencies. I was jealous of the simple Gradle approach and always feel that Maven gets left behind in some of the features and documentation so this really helped me out. 

 

Now if you have an article on how to build Maven-based themes in 7.1 I'd be interested to see that too, as I've found its not that easy....

One thing I found that I'd appreciate your  view on - I started using the 7.1.3 BOMs as listed above but seem to have to "downgrade" the javax.portlet/portlet-api version to 2.0.0 instead of using 3.0.0 as provided. If I don't, I get the Unresolved requirement: Import-Package: javax.portlet; version="[3.0.0,4.0.0)"_ when starting Liferay. Is there something else I need to review/check in my Workspace to make sure I'm not causing this?