Fix duplicate Woodstox / Stax2 JARs in WAR (fixes #2067)#2068
Conversation
…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).
76ab2c1 to
edcb32b
Compare
Test plan resultsRan the full test plan end-to-end. All four checklist items pass. Reproducible recipe below — only Summary
Reproduction recipe — Docker onlyRun 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 Step 1 — Verify the dependency treedocker 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 Step 2 — Build the WAR and inspect itdocker 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): 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 Step 3 — Bake a patched image and start a full stackThe published image expects a JNDI # 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 and zero 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; echoExpected: both REST calls return Cleanupdocker 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-pr2068Things I learned along the way
VerdictFix is correct, minimal, and well-scoped. The exclusions are necessary; the |
Summary
org.codehaus.woodstox:woodstox-core-asl:4.4.1from the three CXF dependencies independencyManagement. This jar is pulled in transitively bycxf-core:3.1.7and ships its own incompatible copy oforg.codehaus.stax2.*classes, colliding with the moderncom.fasterxml.woodstox:woodstox-core:6.2.7already on the classpath viasaaj-impl:1.5.1.<packagingExcludes>WEB-INF/lib/woodstox-core-asl-*.jar</packagingExcludes>safety net tomaven-war-pluginin case a future transitive dependency re-introduces the obsolete jar.Fixes the
NoSuchMethodErroronorg.codehaus.stax2.ri.EmptyIterator.getInstance()thrown during Spring/HibernatesessionFactoryinitialization 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-pluginbanDuplicateClassesrule to catch this category of issue at build time.Verification
mvn -pl libreplan-webapp -am dependency:tree | grep -iE 'woodstox|stax'now reports only:No more
woodstox-core-asl:4.4.1anywhere in the resolved tree.Test plan
mvn clean package -P postgresql,dev,i18n,reports -DskipTests -Dliquibase.skip=trueproduces a WARunzip -l libreplan-webapp/target/libreplan-webapp.war | grep -E 'woodstox|stax2'shows only the modernwoodstox-core-*.jarandstax2-api-*.jar— nowoodstox-core-asl-*.jarlibreplan/libreplan:1.6.0Docker 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 atlocalhost:8080