Overcoming the "Unresolved requirement: Import-Package" deployment error

One blog entry to rule them all (well, for this problem, at least)

"There is nothing new under the sun" - King Solomon, Ecclesiastes 1

In one sense, this blog entry doesn't contain anything new. In another sense, since it brings pieces of prior entries together, I hope that it provides some new insight for portlet developers to overcome the "Unresolved requirement: Import-Package" deployment error.

Reference Blog Entries

Let's start out by referencing the following prior blog entries (ordered by newest to oldest):

  1. Announcement: Liferay Faces Support for Portlet 3.0, JSF 2.3, CDI, and Liferay CE/DXP 7.4 (by me, 2022)
  2. Archetypes for "thin" JSF portlet WAR artifacts (with optional OSGi+CDI Integration) now available (by me, 2021)
  3. Gradle: compile vs compileOnly vs compileInclude (by Dave Nebinger, 2021)
  4. How to try JSF thin WAR demo portlets (by me, 2020)
  5. Speeding-Up Re-Deployment of WAR Artifacts in Liferay 7.x (by me, 2019)
  6. Finding Bundle Dependencies (by Dave Nebinger, 2018)

Source of The Error: Embedding Dependencies

Why does the "Unresolved requirement: Import-Package" deployment error occur? It's almost always because a portlet developer has embedded a dependency in a WAR or JAR artifact.

It's easy for us to understand how this can happen in a WAR module -- it simply means that we built a WAR with one or more JARs embedded in WEB-INF/lib using the "compile" scope with either Maven or Gradle. But Liferay portlet developers (particularly those that are using MVCPortlet in a JAR-based artifact) will sometimes use the "compileInclude" scope (see blog#3 by Dave) in order to embed a JAR within a JAR.

As a result, this can happen with MVCPortlet (Liferay's MVC Framework), FrameworkPortlet (PortletMVC4Spring), GenericFacesPortlet (Liferay Faces / JSF) or any other framework.

Experiencing The Error

In order to experience the "Unresolved requirement: Import-Package" deployment error, let's follow a typical workflow for a JSF portlet developer. Let's start out by generating a new PrimeFaces project using a Maven archetype generation command found at liferayfaces.org...

Step 1: Generate the project

mvn archetype:generate \
  -DarchetypeGroupId=com.liferay.faces.archetype \
  -DarchetypeArtifactId=com.liferay.faces.archetype.primefaces.portlet \
  -DarchetypeVersion=8.0.0 \
  -DgroupId=com.mycompany \
  -DartifactId=com.mycompany.my.jsf.portlet

Step 2: Add some dependencies 

Let's add Apache POI and PrimeFaces Extensions to pom.xml, since they are very typical dependencies:

 <dependency>
    <groupId>org.apache.poi</groupId>
    <artifactId>poi</artifactId>
    <version>5.2.2</version>
</dependency>
<dependency>
    <groupId>org.primefaces.extensions</groupId>
    <artifactId>primefaces-extensions</artifactId>
    <version>10.0.0</version>
</dependency>

Step 3: Build the project with the "thin" profile

mvn -P thin package

Step 4: Examine the embedded dependencies

ls -1 target/com.mycompany.my.primefaces.portlet-1.0.0*/WEB-INF/lib

Directory Listing:

  • SparseBitSet-1.2.jar
  • commons-codec-1.15.jar
  • commons-collections4-4.4.jar
  • commons-io-2.11.0.jar
  • commons-math3-3.6.1.jar
  • log4j-api-2.17.2.jar
  • poi-5.2.2.jar
  • primefaces-extensions-11.0.6.jar

Step 5: Deploy the WAR

cp target/*.war $LIFERAY_HOME/deploy

Step 6: Examine the console log

This is the point at which you would encounter the "Unresolved requirement: Import-Package" deployment error:

com.liferay.portal.kernel.log.LogSanitizerException: org.osgi.framework.BundleException: Could not resolve module: com.mycompany.my.primefaces.portlet [1679]_  Unresolved requirement: Import-Package: org.apache.poi.xssf.usermodel; resolution:="optional"_  Unresolved requirement: Import-Package: com.google.gson_ [Sanitized]
    at org.eclipse.osgi.container.Module.start(Module.java:444) ~[org.eclipse.osgi.jar:?]
    at org.eclipse.osgi.internal.framework.EquinoxBundle.start(EquinoxBundle.java:428) ~[org.eclipse.osgi.jar:?]
    at com.liferay.portal.file.install.internal.DirectoryWatcher._startBundle(DirectoryWatcher.java:1156) [bundleFile:?]
    at com.liferay.portal.file.install.internal.DirectoryWatcher._startBundles(DirectoryWatcher.java:1189) [bundleFile:?]
    at com.liferay.portal.file.install.internal.DirectoryWatcher._startAllBundles(DirectoryWatcher.java:1130) [bundleFile:?]
    at com.liferay.portal.file.install.internal.DirectoryWatcher._process(DirectoryWatcher.java:1041) [bundleFile:?]
    at com.liferay.portal.file.install.internal.DirectoryWatcher.run(DirectoryWatcher.java:221) [bundleFile:?]

Fixing the Error for a single, problematic package

There are two basic approaches to fixing this error, each with benefits and drawbacks:

Approach #1 (Globally / all artifacts): Follow my advice from blog#5, by adding com.google.gson to the module.framework.web.servlet.annotation.scanning.blacklist property.

Potential Benefit: If you have multiple artifacts, this provides a centralized place that fixes the error for all of them.

Potential Benefit: The deployment process will get faster, because the WAB generator will bypass scanning of classes in all of the blacklisted package.

Potential Drawback: You might not have the privilege in your DevOps to set this property.

Potential Drawback: It's unlikely, but you might not want the package blacklisted for all deployed artifacts.

Potential Drawback: Every time you add a dependency, you will likely have to add packages, which affects all deployed artifacts (including Liferay OOTB ones!), which could affect your test scenarios.

Approach #2 (Individual artifact):

Add the package to the Import-Package directive in src/main/webapp/WEB-INF/liferay-plugin-package.properties as resolution:=optional:

Import-Package:\
  com.google.gson;resolution:=optional,\
  *

NOTE: For "thin" JSF portlets, we have the ${osgi.import.package} variable in liferay-plugin-package.properties, so you would need to setup the Import-Package directive in pom.xml instead! (Scroll down to the bottom to the section titled "Setting up pom.xml" to learn how).

Potential Benefit: You are able to isolate the fix to a specific WAR artifact, rather than affecting all deployed artifacts.

Potential Drawback: You have to add this to every WAR artifact that encounters the problem for the package.

Drawback: It doesn't speed up deployment to the degree that Approach#1 does.

How to discover ALL of the problematic packages

Whether you choose Approach #1 or Approach #2, you will find it very tedious to redeploy the artifact over-and-over in order to find the next problematic package.

By following Dave's advice from blog#6, you can discover ALL of the problematic packages and save yourself a lot of time by avoiding the rinse/repeat cycle of redeployment.

Whether you deploy a WAR artifact or a JAR artifact, ultimately a new OSGi bundle will appear in $LIFERAY_HOME/osgi/state. When you deploy a new module, it's normally the sub-directory with the highest integer value. Under there, you will find a "bundleFile" that contains the OSGi metadata you need.

Example Command:

bnd print --impexp ~/portals/liferay-dxp-7.4.13.u31/osgi/state/org.eclipse.osgi/1679/1/bundleFile

Command Output:

[IMPEXP]
Import-Package
  com.google.gson
  com.google.i18n.phonenumbers
  com.ibm.uvm.tools                      {resolution:=optional}
  com.ibm.websphere.wsoc                 {resolution:=optional}
  com.liferay.faces.bridge.ext
  com.liferay.faces.bridge.impl
  com.liferay.petra.io.unsync
  com.liferay.petra.lang
  com.liferay.petra.string
  com.liferay.portal.kernel.bean
  com.liferay.portal.kernel.configuration
  com.liferay.portal.kernel.dao.jdbc
  com.liferay.portal.kernel.dao.orm
  com.liferay.portal.kernel.io
  com.liferay.portal.kernel.io.unsync
  com.liferay.portal.kernel.log
  com.liferay.portal.kernel.portlet
  com.liferay.portal.kernel.portlet.bridges.mvc
  com.liferay.portal.kernel.servlet
  com.liferay.portal.kernel.servlet.filters.invoker
  com.liferay.portal.kernel.template
  com.liferay.portal.kernel.util
  com.liferay.portal.kernel.xml
  com.liferay.portal.model               {resolution:=optional}
  com.liferay.portal.osgi.web.servlet.jsp.compiler {resolution:=optional}
  com.liferay.portal.security.access.control {resolution:=optional}
  com.liferay.portal.security.auth       {resolution:=optional}
  com.liferay.portal.security.permission {resolution:=optional}
  com.liferay.portal.service             {resolution:=optional}
  com.liferay.portal.servlet             {resolution:=optional}
  com.liferay.portal.servlet.filters.aggregate {resolution:=optional}
  com.liferay.portal.servlet.filters.dynamiccss {resolution:=optional}
  com.liferay.portal.spring.context      {resolution:=optional}
  com.liferay.portal.theme               {resolution:=optional}
  com.liferay.portal.util                {resolution:=optional}
  com.liferay.portlet                    {resolution:=optional}
  com.liferay.taglib.aui
  com.liferay.taglib.portlet
  com.liferay.taglib.portletext
  com.liferay.taglib.security
  com.liferay.taglib.theme
  com.liferay.taglib.ui
  com.liferay.taglib.util
  com.sun.el                             {resolution:=optional}
  com.sun.el.lang                        {resolution:=optional}
  com.sun.el.parser                      {resolution:=optional}
  com.sun.el.stream                      {resolution:=optional}
  com.sun.el.util                        {resolution:=optional}
  com.sun.faces
  dev.morphia
  dev.morphia.query
  dev.morphia.query.experimental.filters
  dev.morphia.query.internal
  io.undertow.websockets.jsr             {resolution:=optional}
  javax.annotation
  javax.crypto
  javax.crypto.spec
  javax.el
  javax.enterprise.context
  javax.faces
  javax.faces.application
  javax.faces.component
  javax.faces.component.behavior
  javax.faces.component.html
  javax.faces.component.visit
  javax.faces.context
  javax.faces.convert
  javax.faces.event
  javax.faces.lifecycle
  javax.faces.model
  javax.faces.render
  javax.faces.validator
  javax.faces.view.facelets
  javax.faces.webapp
  javax.imageio
  javax.imageio.metadata
  javax.imageio.stream
  javax.inject
  javax.portlet
  javax.portlet.faces
  javax.servlet
  javax.servlet.http
  javax.swing
  javax.swing.text
  javax.swing.text.html
  javax.websocket                        {resolution:=optional}
  javax.websocket.server                 {resolution:=optional}
  javax.xml.namespace
  javax.xml.parsers
  javax.xml.stream
  javax.xml.transform
  javax.xml.transform.dom
  javax.xml.transform.stream
  javax.xml.validation
  org.apache.commons.logging
  org.apache.logging.log4j.util.internal
  org.apache.naming.java                 {resolution:=optional}
  org.apache.tomcat.websocket.server     {resolution:=optional}
  org.commonmark.node
  org.commonmark.parser
  org.commonmark.renderer.html
  org.dom4j
  org.dom4j.io
  org.eclipse.core.runtime               {resolution:=optional, x-liferay-compatibility:=spring}
  org.osgi.framework
  org.osgi.framework.wiring
  org.owasp.html
  org.primefaces
  org.primefaces.behavior.ajax
  org.primefaces.behavior.base
  org.primefaces.clientwindow
  org.primefaces.component.api
  org.primefaces.component.breadcrumb
  org.primefaces.component.commandbutton
  org.primefaces.component.menu
  org.primefaces.component.outputpanel
  org.primefaces.component.row
  org.primefaces.config
  org.primefaces.context
  org.primefaces.event
  org.primefaces.expression
  org.primefaces.facelets
  org.primefaces.model
  org.primefaces.model.menu
  org.primefaces.renderkit
  org.primefaces.shaded.json
  org.primefaces.shaded.owasp.encoder
  org.primefaces.util
  org.w3c.dom
  org.xml.sax
  sun.misc
  sun.nio.ch
  weblogic.websocket.tyrus               {resolution:=optional}

Analyzing the output:

  • Ignore all the lines with resolution:=optional as they are already taken care of
  • Ignore all the lines that start with com.liferay
  • Ignore the lines with javax.annotationjavax.eljavax.enterprise.context, and javax.inject since your WAR module is a Java EE module that depends on these packages, and they are provided by OSGi.
  • Ignore the javax.portlet line because your WAR module contains a portlet, and that package is provided by OSGi.
  • Ignore all the lines for packages that you certainly depend on from OSGi -- for example, ignore com.sun.faces, javax.faces, javax.servlet, javax.servlet.http, javax.faces.webapp, and javax.portlet.faces since you are building a "thin" JSF WAR artifact that relies on Mojarra being deployed as an OSGi bundle (see blog#1, blog#2,  and blog#4 above).
  • Ignore all the lines that start with org.primefaces since the PrimeFaces dependency is deployed in $LIFERAY_HOME/osgi/modules (thin mode)
  • Ignore the remaining lines that start with "javax" (for now) because they are likely provided by OSGi, and might possibly be required. Better safe than sorry.
  • Be suspicious and/or skeptical at this point. For example, should I expect OSGi to be providing the remaining packages, or is it more likely that they really aren't required at runtime, or are actually present in WEB-INF/lib as embedded dependencies? Some of them should be obvious, e.g. dev.morphia is a really unusual package to rely on, and probably isn't provided by OSGi.
  • After a process of elimination, and after careful analysis, the following lines need to be added:

      com.google.gson;resolution:=optional,\
      com.google.i18n.phonenumbers;resolution:=optional,\
      com.lowagie.text.*;resolution:=optional,\
      dev.morphia.*;resolution:=optional,\
      org.apache.logging.log4j.util.internal;resolution:=optional,\
      org.commonmark.*;resolution:=optional,\
      org.owasp.html;resolution:=optional,\
      org.primefaces.extensions.model.monacoeditor;resolution:=optional,\
      sun.nio.ch;resolution:=optional,\

Setting up pom.xml

As indicated in the above NOTE, "thin" JSF portlets, have the ${osgi.import.package} variable in liferay-plugin-package.properties, so rather than modify that file directly, you would need to setup the Import-Package directive in pom.xml instead in order to take advantage of the variable substitution done by Maven:

<osgi.import.package>
  Import-Package:\
  ${osgi.import.package.primefaces},\
  com.google.gson;resolution:=optional,\
  com.google.i18n.phonenumbers;resolution:=optional,\
  com.lowagie.text.*;resolution:=optional,\
  dev.morphia.*;resolution:=optional,\
  org.apache.logging.log4j.util.internal;resolution:=optional,\
  org.commonmark.*;resolution:=optional,\
  org.owasp.html;resolution:=optional,\
  org.primefaces.extensions.model.monacoeditor;resolution:=optional,\
  sun.nio.ch;resolution:=optional,\
  ${osgi.import.package.liferay.faces.bridge}
</osgi.import.package>

Conclusion

The "Unresolved requirement: Import-Package" deployment error typically occurs due to embedded dependencies in a WAR or JAR module. It's possible to overcome the problem, but it takes a little bit of time. My recommendation is to use the bnd print --impexp command to determine which package(s) are causing the problem, so as to avoid repetitive redeploys which only reveal one problematic package at a time.