Skip to content

Commit 1fef0e1

Browse files
lukaszlenartclaude
andauthored
fix(convention): WW-5593 handle NoClassDefFoundError in action class scanning (#1469)
The PackageBasedActionConfigBuilder now catches NoClassDefFoundError in addition to ClassNotFoundException when scanning for action classes. This prevents application startup failures when classes have missing optional dependencies (e.g., test classes depending on JUnit). Changes: - Add NoClassDefFoundError to catch block in getActionClassTest() - Improve error message to suggest missing dependencies - Add unit tests for both exception types 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude <noreply@anthropic.com>
1 parent 6d778ac commit 1fef0e1

File tree

3 files changed

+506
-2
lines changed

3 files changed

+506
-2
lines changed

plugins/convention/src/main/java/org/apache/struts2/convention/PackageBasedActionConfigBuilder.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -682,8 +682,9 @@ public boolean test(ClassFinder.ClassInfo classInfo) {
682682

683683
try {
684684
return inPackage && (nameMatches || (checkImplementsAction && org.apache.struts2.action.Action.class.isAssignableFrom(classInfo.get())));
685-
} catch (ClassNotFoundException ex) {
686-
LOG.error("Unable to load class [{}]", classInfo.getName(), ex);
685+
} catch (ClassNotFoundException | NoClassDefFoundError ex) {
686+
LOG.error("Unable to load class [{}]. Perhaps it exists but certain dependencies are not available?",
687+
classInfo.getName(), ex);
687688
return false;
688689
}
689690
}

plugins/convention/src/test/java/org/apache/struts2/convention/PackageBasedActionConfigBuilderTest.java

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,10 @@
1818
*/
1919
package org.apache.struts2.convention;
2020

21+
import org.apache.struts2.result.ActionChainResult;
22+
import org.apache.struts2.util.finder.ClassFinder;
23+
import org.apache.struts2.util.finder.Test;
24+
2125
import jakarta.servlet.ServletContext;
2226
import junit.framework.TestCase;
2327
import org.apache.commons.lang3.StringUtils;
@@ -131,6 +135,120 @@ public void setUp() throws Exception {
131135
.bind();
132136
}
133137

138+
/**
139+
* Tests that NoClassDefFoundError is properly caught and handled
140+
* when scanning action classes with missing dependencies.
141+
* <p>
142+
* This is a regression test for WW-5593: Convention plugin fails with
143+
* NoClassDefFoundError when classes have missing optional dependencies.
144+
*
145+
* @see <a href="https://issues.apache.org/jira/browse/WW-5593">WW-5593</a>
146+
*/
147+
public void testNoClassDefFoundErrorHandling() throws Exception {
148+
// Setup minimal configuration
149+
ResultTypeConfig defaultResult = new ResultTypeConfig.Builder("dispatcher",
150+
ServletDispatcherResult.class.getName()).defaultResultParam("location").build();
151+
PackageConfig strutsDefault = makePackageConfig("struts-default", null, null, "dispatcher",
152+
new ResultTypeConfig[]{defaultResult}, null, null, null, true);
153+
154+
final DummyContainer mockContainer = new DummyContainer();
155+
Configuration configuration = new DefaultConfiguration() {
156+
@Override
157+
public Container getContainer() {
158+
return mockContainer;
159+
}
160+
};
161+
configuration.addPackageConfig("struts-default", strutsDefault);
162+
163+
ActionNameBuilder actionNameBuilder = new SEOActionNameBuilder("true", "-");
164+
ObjectFactory of = new ObjectFactory();
165+
of.setContainer(mockContainer);
166+
167+
mockContainer.setActionNameBuilder(actionNameBuilder);
168+
mockContainer.setConventionsService(new ConventionsServiceImpl(""));
169+
170+
// Create the builder with a package that will trigger NoClassDefFoundError simulation
171+
PackageBasedActionConfigBuilder builder = new PackageBasedActionConfigBuilder(
172+
configuration, mockContainer, of, "false", "struts-default", "false");
173+
builder.setActionPackages("org.apache.struts2.convention.actions.noclass");
174+
builder.setActionSuffix("Action");
175+
176+
DefaultFileManagerFactory fileManagerFactory = new DefaultFileManagerFactory();
177+
fileManagerFactory.setContainer(ActionContext.getContext().getContainer());
178+
fileManagerFactory.setFileManager(new DefaultFileManager());
179+
builder.setFileManagerFactory(fileManagerFactory);
180+
builder.setProviderAllowlist(new ProviderAllowlist());
181+
182+
// The getActionClassTest() method returns a Test that should catch NoClassDefFoundError
183+
Test<ClassFinder.ClassInfo> actionClassTest = builder.getActionClassTest();
184+
185+
// Create a mock ClassInfo that throws NoClassDefFoundError when get() is called
186+
// Note: Class name must NOT end with "Action" suffix to ensure classInfo.get() is actually called
187+
// (otherwise nameMatches=true and the condition short-circuits without calling get())
188+
ClassFinder.ClassInfo mockClassInfo = EasyMock.createMock(ClassFinder.ClassInfo.class);
189+
EasyMock.expect(mockClassInfo.getName()).andReturn("org.apache.struts2.convention.actions.noclass.MissingDependency").anyTimes();
190+
EasyMock.expect(mockClassInfo.get()).andThrow(new NoClassDefFoundError("junit/framework/TestCase"));
191+
EasyMock.replay(mockClassInfo);
192+
193+
// This should return false (class excluded) instead of throwing NoClassDefFoundError
194+
boolean result = actionClassTest.test(mockClassInfo);
195+
assertFalse("NoClassDefFoundError should be caught and class should be excluded", result);
196+
}
197+
198+
/**
199+
* Tests that ClassNotFoundException is still properly handled.
200+
* This is a companion test to testNoClassDefFoundErrorHandling() to ensure
201+
* the existing ClassNotFoundException handling still works.
202+
*/
203+
public void testClassNotFoundExceptionHandling() throws Exception {
204+
// Setup minimal configuration
205+
ResultTypeConfig defaultResult = new ResultTypeConfig.Builder("dispatcher",
206+
ServletDispatcherResult.class.getName()).defaultResultParam("location").build();
207+
PackageConfig strutsDefault = makePackageConfig("struts-default", null, null, "dispatcher",
208+
new ResultTypeConfig[]{defaultResult}, null, null, null, true);
209+
210+
final DummyContainer mockContainer = new DummyContainer();
211+
Configuration configuration = new DefaultConfiguration() {
212+
@Override
213+
public Container getContainer() {
214+
return mockContainer;
215+
}
216+
};
217+
configuration.addPackageConfig("struts-default", strutsDefault);
218+
219+
ActionNameBuilder actionNameBuilder = new SEOActionNameBuilder("true", "-");
220+
ObjectFactory of = new ObjectFactory();
221+
of.setContainer(mockContainer);
222+
223+
mockContainer.setActionNameBuilder(actionNameBuilder);
224+
mockContainer.setConventionsService(new ConventionsServiceImpl(""));
225+
226+
PackageBasedActionConfigBuilder builder = new PackageBasedActionConfigBuilder(
227+
configuration, mockContainer, of, "false", "struts-default", "false");
228+
builder.setActionPackages("org.apache.struts2.convention.actions.noclass");
229+
builder.setActionSuffix("Action");
230+
231+
DefaultFileManagerFactory fileManagerFactory = new DefaultFileManagerFactory();
232+
fileManagerFactory.setContainer(ActionContext.getContext().getContainer());
233+
fileManagerFactory.setFileManager(new DefaultFileManager());
234+
builder.setFileManagerFactory(fileManagerFactory);
235+
builder.setProviderAllowlist(new ProviderAllowlist());
236+
237+
Test<ClassFinder.ClassInfo> actionClassTest = builder.getActionClassTest();
238+
239+
// Create a mock ClassInfo that throws ClassNotFoundException when get() is called
240+
// Note: Class name must NOT end with "Action" suffix to ensure classInfo.get() is actually called
241+
// (otherwise nameMatches=true and the condition short-circuits without calling get())
242+
ClassFinder.ClassInfo mockClassInfo = EasyMock.createMock(ClassFinder.ClassInfo.class);
243+
EasyMock.expect(mockClassInfo.getName()).andReturn("org.apache.struts2.convention.actions.noclass.MissingClass").anyTimes();
244+
EasyMock.expect(mockClassInfo.get()).andThrow(new ClassNotFoundException("org.example.MissingClass"));
245+
EasyMock.replay(mockClassInfo);
246+
247+
// This should return false (class excluded) instead of throwing ClassNotFoundException
248+
boolean result = actionClassTest.test(mockClassInfo);
249+
assertFalse("ClassNotFoundException should be caught and class should be excluded", result);
250+
}
251+
134252
public void testActionPackages() throws MalformedURLException {
135253
run("org.apache.struts2.convention.actions", null, null);
136254
}

0 commit comments

Comments
 (0)