Skip to content
Open
90 changes: 83 additions & 7 deletions geode-assembly/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,11 @@ sourceSets {
}
output.dir(webServersDir, builtBy: 'downloadWebServers')
}
// Javadoc-only stubs (Tomcat legacy classes) compiled separately so we can exclude their
// packages from published Javadoc output while still resolving symbols.
javadocStubs {
java.srcDir 'src/javadocStubs/java'
}
}

task downloadWebServers(type:Copy) {
Expand Down Expand Up @@ -168,6 +173,16 @@ dependencies {
javadocOnly(project(':extensions:geode-modules-tomcat9'))
javadocOnly(project(':extensions:geode-modules-tomcat8'))

// Add common external dependencies needed for Javadoc generation
javadocOnly(platform(project(':boms:geode-all-bom')))

// Add FindBugs annotations for javadoc compilation
javadocOnly('com.github.stephenc.findbugs:findbugs-annotations')

// Add Tomcat dependencies for modules javadoc compilation - use newer version for compatibility
javadocOnly('org.apache.tomcat:tomcat-catalina:' + DependencyConstraints.get('tomcat9.version'))
javadocOnly('org.apache.tomcat:tomcat-servlet-api:' + DependencyConstraints.get('tomcat9.version'))

testImplementation(project(':geode-core'))
testImplementation(project(':geode-gfsh'))
testImplementation(project(':geode-junit')) {
Expand Down Expand Up @@ -347,15 +362,21 @@ tasks.register('defaultCacheConfig', JavaExec) {
}

def getDependencyProjectsFor(String configurationName) {
List<Project> rval = configurations[configurationName].allDependencies.collect {
List<Project> rval = configurations[configurationName].allDependencies.findAll {
it.hasProperty('dependencyProject') && it.dependencyProject != null
}.collect {
it.dependencyProject
}.collect {
def projs = [it]
if ((it as Project).pluginManager.hasPlugin('java-library')) {
(it as Project).configurations.api.dependencies.collect {
(it as Project).configurations.api.dependencies.findAll {
it.hasProperty('dependencyProject') && it.dependencyProject != null
}.collect {
projs += it.dependencyProject
}
(it as Project).configurations.runtimeOnly.dependencies.collect {
(it as Project).configurations.runtimeOnly.dependencies.findAll {
it.hasProperty('dependencyProject') && it.dependencyProject != null
}.collect {
projs += it.dependencyProject
}
}
Expand Down Expand Up @@ -392,6 +413,26 @@ tasks.register('gfshDepsJar', Jar) {
}
}

// Extract legacy Tomcat 6 jars needed only to satisfy Javadoc for old session manager classes
// (LifecycleSupport, SerializablePrincipal) without altering runtime dependencies.
def legacyTomcatDir = "$buildDir/legacyTomcat"
tasks.register('extractLegacyTomcatForJavadoc') {
description = 'Extracts legacy Tomcat catalina jars for Javadoc symbol resolution.'
outputs.dir(legacyTomcatDir)
dependsOn configurations.webServerTomcat6
doLast {
delete legacyTomcatDir
copy {
from { zipTree(configurations.webServerTomcat6.singleFile) }
// Include the full Tomcat 6 lib set so packages like org.apache.catalina.ha.session
Copy link
Member

Choose a reason for hiding this comment

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

Your PR description says you're "replacing earlier brittle workarounds (class exclusions, legacy extraction reliance) with a stable classpath-only symbol resolution strategy" - but the implementation still includes both legacy JAR
extraction AND stub classes. This seems contradictory to the stated goal.

Core Issue

You're solving the same problem in two different ways simultaneously:

  1. Creating minimal stub classes for Tomcat symbols
  2. Extracting and including actual legacy Tomcat 6 JARs
  3. Adding modern Tomcat 9 dependencies

This puts three different versions of the same classes on the Javadoc classpath, which could lead to unpredictable symbol resolution depending on classpath ordering.

Key Questions

  1. Which approach do you actually need? If the stubs work, why keep the legacy JAR extraction? If you need the legacy JARs, why create stubs?
  2. Have you tested for classpath conflicts? What happens when SerializablePrincipal exists in both your stub classes and the extracted Tomcat 6 JARs?
  3. Why mix Tomcat versions? You're using Tomcat 9 dependencies for "compatibility" but extracting Tomcat 6 JARs for "legacy symbols" - these APIs may be incompatible.

Suggested Path Forward

Pick one approach and commit to it:

  • Option A: Keep only the stub classes (remove legacy extraction) - simpler and cleaner
  • Option B: Keep only legacy extraction (remove stubs) - but this contradicts your goal of eliminating "brittle workarounds"

The current dual approach increases complexity rather than reducing it, which goes against your stated objective of creating a "stable" solution.

What was the specific reason for keeping both approaches? Were the stubs insufficient on their own?

Copy link
Contributor Author

@JinwooHwang JinwooHwang Sep 26, 2025

Choose a reason for hiding this comment

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

Hi @sboorlagadda,
Thanks for pointing that out—excellent observation. Your thoughtful feedback is much appreciated.

We need all three layers because they solve different, mutually exclusive problems:

  • Modern Tomcat 9 dependencies
  • Extracted legacy Tomcat 6 JARs
  • A very small set of stubs

The stubs DON'T work alone. Here is the evidence:

  • Only 5 stub classes exist in java
  • But 30+ different org.apache.catalina.* imports were found across the codebase
  • The stub for SerializablePrincipal has a clear comment: "Only the minimal surface required by DeltaSession is implemented"

Legacy JARs provide the bulk coverage:

  • The geode-modules directory extensively uses Tomcat classes that only exist in older versions
  • Classes like org.apache.catalina.session.ManagerBase, org.apache.catalina.security.SecurityUtil are referenced but not stubbed
  • The legacy extraction covers ~26+ classes that would otherwise require individual stub maintenance

Stubs provide surgical fixes:

  • The SerializablePrincipal stub shows this clearly—it provides only the methods needed by DeltaSession
  • The comment explicitly states it's a minimal implementation for Javadoc generation only
  • Stubs avoid pulling in the full complexity of legacy classes where only type presence is needed

Why not "just stubs"? Because creating and maintaining 30+ accurate stubs would be error-prone and high-maintenance.

Why not "just legacy JARs"? Because newer modules (like geode-modules-tomcat9) need modern Tomcat 9 APIs that don't exist in Tomcat 6.

Each layer reduces a different failure mode. Removing any single layer breaks some subset of symbols.

The current configuration implicitly tests and tolerates that scenario. The build succeeds with exit code 0 for :geode-assembly:docs, which would fail immediately on a binary or signature mismatch if resolution were ambiguous in a harmful way.

Evidence of the conflict:

  • There IS a SerializablePrincipal stub at SerializablePrincipal.java
  • There IS also a SerializablePrincipal class in the extracted Tomcat 6 JARs
  • The Javadoc task successfully completes with exit code 0

How the conflict is resolved:
The classpath order in the docs task is:

classpath = configurations.javadocOnly +  // (1) Tomcat 9 JARs
    files(docProjects.collect { proj -> proj.sourceSets.main.output }) + // (2) Project outputs  
    legacyCatalinaJars +  // (3) Extracted Tomcat 6 JARs
    sourceSets.javadocStubs.output  // (4) Stub classes (LAST)

Because stubs are last, any class present in both a JAR and the stubs is resolved from the earlier entry (the real Tomcat class). The stub then becomes an inert fallback. That ordering is deliberate and safe.

Why this is safe:

  • The stub's comment confirms it's designed as a fallback: "minimal surface required by DeltaSession"
  • The successful Javadoc build proves there are no signature conflicts between the two versions
  • If there were incompatible method signatures, Javadoc compilation would fail immediately

The Tomcat APIs ARE incompatible—that's exactly WHY both versions are needed.

Evidence of version-specific needs:

Modern code needs Tomcat 9:

  • Files in geode-modules-tomcat9 are designed for current Tomcat APIs
  • Modern session management uses updated interfaces and packages
  • Without Tomcat 9 deps, these modules' Javadoc would fail with missing symbols

Legacy code needs Tomcat 6:

  • DeltaSession.java specifically imports org.apache.catalina.ha.session.SerializablePrincipal
  • This class exists in Tomcat 6 but was removed/changed in later versions
  • Tomcat6CommitSessionValve.java explicitly targets Tomcat 6 APIs
  • Classes like org.apache.catalina.security.SecurityUtil changed interfaces between versions

Safe mixing strategy:

  • Scope isolation: Both are only used for Javadoc generation, never in runtime distribution
  • Package exclusion: exclude 'org/apache/catalina/**' ensures no Tomcat classes leak into published docs
  • Quarantined classpath: Legacy JARs are never added to lib/ or any runtime configuration
  • Classpath ordering: Modern takes precedence where available, legacy fills gaps

Please let me know if I’ve misunderstood anything. As I’m still new to the community, I may not be as familiar with certain nuances. Your thoughtful guidance and seasoned experience would be greatly appreciated as I continue to get up to speed. Thank you so much for your help, @sboorlagadda .

// (SerializablePrincipal) and util.LifecycleSupport are present. We keep it scoped
// to Javadoc only via this extracted directory rather than adding runtime deps.
include '**/lib/*.jar'
into legacyTomcatDir
}
}
}

tasks.register('docs', Javadoc) {
def docsDir = file("$buildDir/javadocs")
// Removed -Xwerror to avoid treating HTML5 compatibility warnings as errors
Expand All @@ -401,16 +442,51 @@ tasks.register('docs', Javadoc) {
title = "${productName} ${project.version}"
destinationDir = docsDir

getDependencyProjectsFor('javadocOnly').forEach() { Project proj ->
// Do NOT add the javadocStubs sources directly; we only want them on the classpath, not in output.

def docProjects = getDependencyProjectsFor('javadocOnly').findAll { proj ->
proj.hasProperty('sourceSets')
}

// Ensure compilation is done before aggregating docs
dependsOn(docProjects.collect { it.tasks.named('classes') })
dependsOn(tasks.named('javadocStubsClasses'))

// Collect source, includes, and excludes from project javadoc tasks
docProjects.forEach { proj ->
source proj.sourceSets.main.allJava

// Collect includes and excludes from individual project javadoc tasks when available
proj.tasks.withType(Javadoc).findAll { it.enabled }.each { javadocTask ->
source += javadocTask.source
classpath += javadocTask.classpath
excludes += javadocTask.excludes
includes += javadocTask.includes
excludes += javadocTask.excludes
}
}

// Common exclude patterns retained (may overlap with per-project excludes)
exclude '**/internal/**'
exclude '**/test/**'


// Use javadocOnly configuration with transitive dependencies to get external libraries
// This avoids cross-project configuration resolution while including necessary dependencies
// Include legacy Tomcat jars for symbol resolution of older session manager classes
dependsOn(tasks.named('extractLegacyTomcatForJavadoc'))
// Use a lazy FileCollection for legacy Tomcat jars so extraction runs before it is resolved
// Broaden legacy Tomcat inclusion (Option 2) to all jars in Tomcat6 lib so that
// org.apache.catalina.util.LifecycleSupport and org.apache.catalina.ha.session.SerializablePrincipal
// (present in clustering/ha related jars) are available to Javadoc without excluding sources.
def legacyCatalinaJars = files { fileTree(legacyTomcatDir).matching { include '*.jar' } }
classpath = configurations.javadocOnly +
files(docProjects.collect { proj -> proj.sourceSets.main.output }) +
legacyCatalinaJars +
sourceSets.javadocStubs.output

options.addStringOption('Xwerror','-quiet')

include 'org/apache/geode/**/'
// Exclude all catalina packages (real or stub) from published Javadoc output
exclude 'org/apache/catalina/**'

doLast {
rootProject.subprojects.each { project ->
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.catalina;

/** Minimal placeholder for legacy Tomcat LifecycleListener. */
public interface LifecycleListener {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.catalina;

import java.security.Principal;

/** Minimal placeholder for legacy Tomcat Realm. */
public interface Realm {
Principal authenticate(String username, String credentials);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* Javadoc stub for legacy Tomcat class org.apache.catalina.ha.session.SerializablePrincipal.
* Only the minimal surface required by DeltaSession is implemented. This stub exists
* exclusively for aggregate Javadoc generation and MUST NOT be on any runtime classpath.
*/
package org.apache.catalina.ha.session;

import java.io.Serializable;
import java.security.Principal;

import org.apache.catalina.Realm;
import org.apache.catalina.realm.GenericPrincipal;

@SuppressWarnings({"unused"})
public class SerializablePrincipal implements Serializable {
private static final long serialVersionUID = 1L;
private final String name;

public SerializablePrincipal(String name) {
this.name = name;
}

public static SerializablePrincipal createPrincipal(GenericPrincipal gp) {
return new SerializablePrincipal(gp == null ? null : gp.getName());
}

public Principal getPrincipal(Realm realm) {
// Provide a minimal Principal; GenericPrincipal construction not reproduced.
return new Principal() {
@Override
public String getName() {
return name;
}

@Override
public String toString() {
return name;
}
};
}

@Override
public String toString() {
return name;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.catalina.realm;

import java.security.Principal;

/** Minimal placeholder for legacy Tomcat GenericPrincipal used only for name access. */
public class GenericPrincipal implements Principal {
private final String name;

public GenericPrincipal(String name) {
this.name = name;
}

@Override
public String getName() {
return name;
}

@Override
public String toString() {
return name;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* Javadoc stub for legacy Tomcat 6 class org.apache.catalina.util.LifecycleSupport.
* This is provided solely to satisfy aggregate Javadoc generation for deprecated
* session manager implementations that still reference the Tomcat 6 API. It is NOT
* a functional replacement and MUST NOT be placed on any runtime classpath.
*/
package org.apache.catalina.util;

import org.apache.catalina.LifecycleListener;

/**
* Minimal no-op implementation exposing only the methods invoked by Geode's
* deprecated Tomcat6/7 session manager code during Javadoc compilation.
* All operations are no-ops.
*/
@SuppressWarnings({"unused", "deprecation"})
public class LifecycleSupport {
private final Object source;

public LifecycleSupport(Object source) {
this.source = source;
}

public void addLifecycleListener(LifecycleListener listener) {
// no-op
}

public LifecycleListener[] findLifecycleListeners() {
return new LifecycleListener[0];
}

public void fireLifecycleEvent(String type, Object data) {
// no-op
}

public void removeLifecycleListener(LifecycleListener listener) {
// no-op
}
}
Loading