Skip to content

Fix duplicate Woodstox / Stax2 JARs in WAR (fixes #2067)#2068

Merged
kwoot merged 1 commit into
LibrePlan:mainfrom
sanbor:fix-woodstox-stax2-conflict-2067
May 21, 2026
Merged

Fix duplicate Woodstox / Stax2 JARs in WAR (fixes #2067)#2068
kwoot merged 1 commit into
LibrePlan:mainfrom
sanbor:fix-woodstox-stax2-conflict-2067

Conversation

@sanbor

@sanbor sanbor commented May 20, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Excludes the obsolete org.codehaus.woodstox:woodstox-core-asl:4.4.1 from the three CXF dependencies in dependencyManagement. This jar is pulled in transitively by cxf-core:3.1.7 and ships its own incompatible copy of org.codehaus.stax2.* classes, colliding with the modern com.fasterxml.woodstox:woodstox-core:6.2.7 already on the classpath via saaj-impl:1.5.1.
  • Adds a <packagingExcludes>WEB-INF/lib/woodstox-core-asl-*.jar</packagingExcludes> safety net to maven-war-plugin in case a future transitive dependency re-introduces the obsolete jar.

Fixes the NoSuchMethodError on org.codehaus.stax2.ri.EmptyIterator.getInstance() thrown during Spring/Hibernate sessionFactory initialization that prevents the 1.6.0 Docker image from starting on Tomcat 9 / JDK 21.

Fixes #2067. Diagnosis credit to @Interfud.

A follow-up PR will add a maven-enforcer-plugin banDuplicateClasses rule to catch this category of issue at build time.

Verification

mvn -pl libreplan-webapp -am dependency:tree | grep -iE 'woodstox|stax' now reports only:

|     +- org.codehaus.woodstox:stax2-api:jar:4.2.1:compile
|     \- com.fasterxml.woodstox:woodstox-core:jar:6.2.7:compile
|  \- org.jvnet.staxex:stax-ex:jar:1.8.1:compile

No more woodstox-core-asl:4.4.1 anywhere in the resolved tree.

Test plan

  • mvn clean package -P postgresql,dev,i18n,reports -DskipTests -Dliquibase.skip=true produces a WAR
  • unzip -l libreplan-webapp/target/libreplan-webapp.war | grep -E 'woodstox|stax2' shows only the modern woodstox-core-*.jar and stax2-api-*.jar — no woodstox-core-asl-*.jar
  • Rebuild the libreplan/libreplan:1.6.0 Docker image, run with the docker-compose stack from issue there are two different versions of the XML processing library (Stax2 / Woodstox). #2067, and confirm the Spring context starts cleanly and the app responds at localhost:8080
  • Existing CXF JAX-RS endpoints still function (smoke-test web service entry points)

…brePlan#2067).

cxf-core:3.1.7 declares org.codehaus.woodstox:woodstox-core-asl:4.4.1 as a
direct dependency, which ships its own copy of org.codehaus.stax2.* classes.
Combined with the modern com.fasterxml.woodstox:woodstox-core (already on the
classpath via saaj-impl), this causes a NoSuchMethodError on
org.codehaus.stax2.ri.EmptyIterator.getInstance() during Spring/Hibernate
bootstrap, making the 1.6.0 Docker image fail to start.

Exclude woodstox-core-asl from the three CXF dependencies in
dependencyManagement and add a maven-war-plugin packagingExcludes safety net.

Why this wasn't caught before:

1. Maven can't dedup these — the groupIds are different.
   The two JARs are:
   - org.codehaus.woodstox:woodstox-core-asl:4.4.1 (obsolete)
   - com.fasterxml.woodstox:woodstox-core:6.2.7   (modern)
   Maven's conflict resolution only kicks in when groupId:artifactId
   matches. These look like different artifacts to it, so both get
   packaged. There's no warning, no dependency convergence error.

2. Both ship overlapping package org.codehaus.stax2.ri.
   The newer stax2-api-4.2.1.jar has EmptyIterator.getInstance(). The
   legacy woodstox-core-asl-4.4.1.jar contains its OWN older copy of
   org.codehaus.stax2.ri.EmptyIterator — without that static method.
   When two JARs both contribute the same class, the JVM picks whichever
   the classloader iterates to first.

3. Classloader iteration order is environment-dependent.
   - In Jetty + Maven (local dev), classpath order follows Maven's
     dependency resolution — typically the "well-known" entries get
     priority.
   - In Tomcat 9 with WAR deployment (the reporter's case), JARs in
     WEB-INF/lib/ are loaded via filesystem enumeration. On Linux ext4
     that's often inode/insertion order, not alphabetical — so
     woodstox-core-asl-4.4.1.jar can win the race for EmptyIterator
     even though the modern stax2-api is also there.

   So the bug only triggers when the unlucky classloader picks the old
   EmptyIterator AND modern code calls .getInstance() on it. That combo
   requires (a) both JARs on the classpath, (b) Tomcat-style
   classloader, (c) a code path through Hibernate's StAX setup that
   hits EmptyIterator.getInstance().

4. The project's testing path doesn't exercise it.
   - pom.xml targets Java 1.8 — contributors run JDK 8 + Jetty
     (mvn jetty:run), not Tomcat 9 + JDK 21.
   - The Docker 1.6.0 image (Tomcat 9 + Linux) is where this
     combination lives, and that's also where the issue was reported.
   - No Maven Enforcer rule (dependencyConvergence / banDuplicateClasses)
     configured to flag overlapping classes at build time.

5. The modern Woodstox arrived via a quiet path.
   com.fasterxml.woodstox:woodstox-core:6.2.7 is pulled in transitively
   by saaj-impl:1.5.1 (declared in libreplan-webapp/pom.xml). Before
   that dep was on the classpath, only the old woodstox-core-asl
   existed — no conflict, no bug. The conflict was latent until
   something pulled in the modern Woodstox; nobody connected the dots.

Diagnosis credit: Interfud (LibrePlan#2067).
@sanbor sanbor force-pushed the fix-woodstox-stax2-conflict-2067 branch from 76ab2c1 to edcb32b Compare May 20, 2026 20:14
@sanbor

sanbor commented May 20, 2026

Copy link
Copy Markdown
Contributor Author

Test plan results

Ran the full test plan end-to-end. All four checklist items pass. Reproducible recipe below — only docker and curl required, no host JDK/Maven.

Summary

# Test plan item Result
1 mvn clean package -P postgresql,dev,i18n,reports … produces a WAR BUILD SUCCESS
2 unzip -l of the WAR shows only modern woodstox-core-* / stax2-api-*, no woodstox-core-asl-* ✅ See output below
3 Rebuild libreplan/libreplan:1.6.0 image, run docker-compose, Spring context starts cleanly, app responds at localhost:8080 ✅ Tomcat logs Server startup in [~65000] ms, login page renders
4 Existing CXF JAX-RS endpoints still function GET /ws/rest/calendars/ and GET /ws/rest/unittypes/ both return HTTP 200 with valid XML

Reproduction recipe — Docker only

Run every step from the repo root after checking out this branch. All build/seed steps run inside containers, so you don't need Maven or a JDK on the host. Files Maven writes under libreplan-{webapp,business}/target/ will be owned by root (that's the container's default user) — clean them up with docker run --rm -v "$PWD":/r alpine rm -rf /r/libreplan-webapp/target /r/libreplan-business/target if you care.

Step 1 — Verify the dependency tree

docker run --rm \
  -v "$PWD":/repo -w /repo \
  -v libreplan-m2:/root/.m2 \
  maven:3.9.7-eclipse-temurin-11 \
  mvn -pl libreplan-webapp -am dependency:tree -q | grep -iE 'woodstox|stax'

Expected (matches PR description — no woodstox-core-asl:4.4.1):

|     +- org.codehaus.woodstox:stax2-api:jar:4.2.1:compile
|     \- com.fasterxml.woodstox:woodstox-core:jar:6.2.7:compile
|  \- org.jvnet.staxex:stax-ex:jar:1.8.1:compile

Step 2 — Build the WAR and inspect it

docker run --rm \
  -v "$PWD":/repo -w /repo \
  -v libreplan-m2:/root/.m2 \
  maven:3.9.7-eclipse-temurin-11 \
  mvn clean package -P postgresql,dev,i18n,reports -DskipTests -Dliquibase.skip=true

# Inspect the new WAR (uses jar from the temurin image)
docker run --rm \
  -v "$PWD/libreplan-webapp/target":/w \
  eclipse-temurin:11 \
  jar tf /w/libreplan-webapp.war | grep -iE 'woodstox|stax'

Expected (only modern jars):

WEB-INF/lib/stax2-api-4.2.1.jar
WEB-INF/lib/stax-ex-1.8.1.jar
WEB-INF/lib/woodstox-core-6.2.7.jar

For contrast — confirm the currently published 1.6.0 image bundles both woodstox versions (the bug):

docker run --rm libreplan/libreplan:1.6.0 \
  jar tf /usr/local/tomcat/webapps/ROOT.war | grep -iE 'woodstox|stax'

Outputs (note the obsolete woodstox-core-asl-4.4.1.jar):

WEB-INF/lib/woodstox-core-asl-4.4.1.jar
WEB-INF/lib/stax2-api-4.2.1.jar
WEB-INF/lib/woodstox-core-6.2.7.jar
WEB-INF/lib/stax-ex-1.8.1.jar

Step 3 — Bake a patched image and start a full stack

The published image expects a JNDI DataSource reachable at hostname db, and does not run Liquibase on startup — you must seed the schema first, otherwise Hibernate aborts with Schema-validation: missing table [advance_assignment] (true with or without this PR).

# 3a — Create a tiny build context for the patched image
mkdir -p /tmp/libreplan-pr2068
cp libreplan-webapp/target/libreplan-webapp.war /tmp/libreplan-pr2068/

cat > /tmp/libreplan-pr2068/Dockerfile <<'EOF'
FROM libreplan/libreplan:1.6.0
USER root
RUN rm -rf /usr/local/tomcat/webapps/ROOT.war /usr/local/tomcat/webapps/ROOT
COPY libreplan-webapp.war /usr/local/tomcat/webapps/ROOT.war
COPY ROOT.xml /usr/local/tomcat/conf/Catalina/localhost/ROOT.xml
EOF

cat > /tmp/libreplan-pr2068/ROOT.xml <<'EOF'
<Context antiJARLocking="true">
  <Resource name="jdbc/libreplan-ds" auth="Container"
            type="javax.sql.DataSource"
            maxActive="100" maxIdle="30" maxWait="10000"
            username="libreplan" password="libreplan"
            driverClassName="org.postgresql.Driver"
            url="jdbc:postgresql://db:5432/libreplandev" />
</Context>
EOF

docker build -t libreplan:pr2068 /tmp/libreplan-pr2068

# 3b — Network + Postgres
docker network create libreplan-pr2068-net

docker run -d --name libreplan-pr2068-db \
  --network libreplan-pr2068-net --network-alias db \
  -e POSTGRES_USER=libreplan -e POSTGRES_PASSWORD=libreplan \
  -e POSTGRES_DB=libreplandev \
  postgres:18

until docker exec libreplan-pr2068-db pg_isready -U libreplan -d libreplandev 2>&1 \
  | grep -q accepting; do sleep 1; done

# 3c — Seed schema via Liquibase, all inside a maven container on the same network
docker run --rm \
  -v "$PWD":/repo -w /repo \
  -v libreplan-m2:/root/.m2 \
  --network libreplan-pr2068-net \
  maven:3.9.7-eclipse-temurin-11 \
  bash -c '
    set -e
    mvn -pl libreplan-business -am process-resources -P postgresql,dev -DskipTests -q
    sed -i "s|jdbc:postgresql://localhost/libreplandev|jdbc:postgresql://db:5432/libreplandev|" \
        libreplan-business/target/classes/liquibase.properties
    cd libreplan-business
    mvn liquibase:update -P postgresql,dev \
      -Dliquibase.propertyFile=target/classes/liquibase.properties
  '

# 3d — Start the patched LibrePlan
docker run -d --name libreplan-pr2068 \
  --network libreplan-pr2068-net \
  -p 8080:8080 \
  libreplan:pr2068

# 3e — Wait for terminal startup state, then confirm no woodstox error
until docker logs libreplan-pr2068 2>&1 \
  | grep -qE 'Server startup in|listeners failed|NoSuchMethodError'; do sleep 5; done

docker logs libreplan-pr2068 2>&1 \
  | grep -E 'NoSuchMethodError|EmptyIterator|Server startup in'

Expected: a single line ending with

Server startup in [~65000] milliseconds

and zero NoSuchMethodError / EmptyIterator matches.

Step 4 — Smoke-test the app and CXF JAX-RS endpoints

# Login page renders (302 → /common/layout/login.zul → 200)
curl -sI -L http://localhost:8080/ | grep -E '^(HTTP|Location)'

# CXF REST: list base calendars (seeded by Liquibase)
curl -s -u wsreader:wsreader -H 'Accept: application/xml' \
  http://localhost:8080/ws/rest/calendars/ | head -c 300; echo

# CXF REST: list unit types
curl -s -u wsreader:wsreader -H 'Accept: application/xml' \
  http://localhost:8080/ws/rest/unittypes/ | head -c 300; echo

Expected: both REST calls return <?xml version="1.0" …?><base-calendar-list …> and <unit-type-list …> payloads. Default seeded credentials are wsreader:wsreader (read) / wswriter:wswriter (write); the admin user does not have the ROLE_WS_READER role required by /ws/rest/**.

Cleanup

docker rm -f libreplan-pr2068 libreplan-pr2068-db
docker network rm libreplan-pr2068-net
docker rmi libreplan:pr2068
docker volume rm libreplan-m2          # optional: drops the maven cache
rm -rf /tmp/libreplan-pr2068

Things I learned along the way

  1. The bug as reported (NoSuchMethodError on EmptyIterator.getInstance()) is classpath-order sensitive. I tried to reproduce the original crash on three configurations — the published libreplan/libreplan:1.6.0 image (JDK 11 + Tomcat 9 + duplicate jars), a hand-built tomcat:9-jdk21 image with the unpatched WAR, and the same on JDK 21 with a seeded DB. In every case the classloader happened to load woodstox-core-6.2.7.jar first and the original error never surfaced. The reporters (Debian 13 + JDK 21, and a second user on the stock image) got the opposite ordering and hit the crash. This explains why some users see it and others don't — the failure depends on filesystem iteration order at classpath construction time. The PR's value is exactly that it makes the conflict structurally impossible, so ordering can no longer matter.

  2. The published image runs JDK 11, not JDK 21 as the PR description hints. docker inspect libreplan/libreplan:1.6.0 shows JAVA_VERSION=jdk-11.0.31+11, TOMCAT_VERSION=9.0.118. The "Tomcat 9 / JDK 21" combination from issue there are two different versions of the XML processing library (Stax2 / Woodstox). #2067 is what one of the reporters built locally; the stock image is JDK 11. So verifying on the stock image doesn't fully exercise the JDK-21 code path — but the structural fix (removing the duplicate jar) is JDK-version-agnostic.

  3. JDK 8 is too old to compile this project, even though <source>/<target> is still 1.8. The build fails with class file has wrong version 55.0, should be 52.0 on com.libreplan.java.zk.components:jasperreportcomponent:1.2, which is shipped pre-compiled for Java 11. HACKING.rst documents JDK 11 as the build requirement — make sure the maven image you use is eclipse-temurin-11 (not 8).

  4. The published libreplan/libreplan:1.6.0 image does not run Liquibase on startup. The webapp deploys with hibernate.hbm2ddl.auto=validate, so an unseeded DB fails with Schema-validation: missing table [advance_assignment] and the context never starts. This is independent of there are two different versions of the XML processing library (Stax2 / Woodstox). #2067 — you'll hit it with or without this PR if you don't seed the schema first. The Liquibase migration step in the recipe above does that.

  5. The JNDI DataSource hostname is db. The published image's ROOT.xml references jdbc:postgresql://db:5432/libreplan, so the Postgres container must be reachable as hostname db (network alias or compose service name). The DB_HOST/DB_NAME environment variables in the docker-compose snippet from issue there are two different versions of the XML processing library (Stax2 / Woodstox). #2067 are not honored by the image — they're decoration. Also: the dev profile uses DB name libreplandev, not libreplan, so the ROOT.xml above overrides the URL accordingly.

  6. CXF endpoints require Spring Security HTTP Basic with the right role. intercept-url pattern="/ws/rest/**" requires ROLE_WS_READER (GET) or ROLE_WS_WRITER (POST/DELETE). The seeded admin user does not have those roles by default — use wsreader/wswriter (default password = loginname after a fresh Liquibase seed) for smoke testing.

  7. The PR's packagingExcludes safety net works as advertised. It's enforced at war plugin packaging time — even if a future transitive change re-introduces woodstox-core-asl-*.jar, the WAR will exclude it. The exclusion-by-name in the dependencyManagement block is the primary fix; packagingExcludes is belt-and-braces.

  8. Sanity-check trick: the published libreplan/libreplan:1.6.0 image is a JDK image, so jar tf works inline on its bundled WAR (see Step 2) — no need to extract the WAR first. Same trick works on any eclipse-temurin / maven container for the freshly built artifact.

Verdict

Fix is correct, minimal, and well-scoped. The exclusions are necessary; the packagingExcludes is good defense-in-depth. The forthcoming banDuplicateClasses enforcer rule mentioned in the PR description would be a worthwhile follow-up to catch this category of issue at build time, as already noted.

@kwoot kwoot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@kwoot kwoot merged commit 897d936 into LibrePlan:main May 21, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

there are two different versions of the XML processing library (Stax2 / Woodstox).

2 participants