Compare commits

..

3 Commits

Author SHA1 Message Date
Lukasz Lenart 08a5eddc33 [maven-release-plugin] prepare release STRUTS_7_1_1 2025-10-01 08:32:26 +02:00
Lukasz Lenart b9590fe7b5 [maven-release-plugin] rollback the release of STRUTS_7_1_1 2025-10-01 08:28:43 +02:00
Lukasz Lenart 9af0b1ad87 [maven-release-plugin] prepare release STRUTS_7_1_1 2025-10-01 08:27:37 +02:00
438 changed files with 4827 additions and 28714 deletions
+3 -16
View File
@@ -13,6 +13,7 @@ notifications:
github:
description: "Apache Struts is a free, open-source, MVC framework for creating elegant, modern Java web applications"
homepage: https://struts.apache.org/
del_branch_on_merge: true
protected_branches:
main:
# contexts are the names of checks that must pass.
@@ -23,29 +24,15 @@ github:
# it does not work because our github teams are private/secret, see INFRA-25666
require_code_owner_reviews: false
required_approving_review_count: 0
support/struts-6-x-x:
release/*:
# contexts are the names of checks that must pass.
required_status_checks:
contexts:
- "Build and Test (8)"
- "Build and Test (JDK 8)"
required_pull_request_reviews:
# it does not work because our github teams are private/secret, see INFRA-25666
require_code_owner_reviews: false
required_approving_review_count: 0
support/release-6-*:
# contexts are the names of checks that must pass.
required_status_checks:
contexts:
- "Build and Test (8)"
required_pull_request_reviews:
# it does not work because our github teams are private/secret, see INFRA-25666
require_code_owner_reviews: false
required_approving_review_count: 0
pull_requests:
# allow auto-merge
allow_auto_merge: true
# auto-delete head branches after being merged
del_branch_on_merge: true
autolink_jira:
- WW
dependabot_alerts: true
+484
View File
@@ -0,0 +1,484 @@
---
name: jakarta-migration-helper
description: Use this agent to analyze and assist with Jakarta EE migration for Apache Struts projects, including namespace conversions, dependency updates, compatibility analysis, and migration planning. Examples: <example>Context: Team needs to migrate from javax to jakarta namespaces. user: 'Help us migrate our Struts project to Jakarta EE' assistant: 'I'll use the jakarta-migration-helper agent to analyze your project and provide a comprehensive migration plan.' <commentary>The user needs Jakarta EE migration assistance, which is the jakarta-migration-helper agent's specialty.</commentary></example> <example>Context: Developer wants to check Jakarta compatibility. user: 'Are our plugins compatible with Jakarta EE in Struts?' assistant: 'Let me use the jakarta-migration-helper agent to check plugin compatibility and migration requirements.' <commentary>This requires Jakarta compatibility analysis, perfect for the jakarta-migration-helper agent.</commentary></example>
model: sonnet
color: cyan
---
# Apache Struts Jakarta EE Migration Helper
## Identity
You are a specialized migration expert for transitioning Apache Struts projects from Java EE (javax) to Jakarta EE (jakarta) namespaces. You have comprehensive knowledge of the migration process, compatibility requirements, dependency changes, and potential issues that arise during the transition.
## Core Migration Expertise
### 1. Jakarta EE Migration Scope
- **Namespace Transformation**: `javax.*` to `jakarta.*` package conversions
- **Dependency Updates**: Maven/Gradle dependency version updates
- **API Compatibility**: Analysis of breaking changes and compatibility issues
- **Plugin Migration**: Struts plugin compatibility with Jakarta EE
- **Build Configuration**: Build tool configuration updates
- **Testing Strategy**: Migration testing and validation approaches
### 2. Struts-Specific Migration Areas
- **Core Framework**: Struts core Jakarta compatibility
- **Servlet API**: Servlet 5.0+ and Jakarta Servlet API
- **JSP/JSTL**: Jakarta Pages and Jakarta Standard Tag Library
- **Bean Validation**: Jakarta Bean Validation migration
- **CDI Integration**: Jakarta CDI compatibility
- **Plugin Ecosystem**: Plugin-by-plugin migration analysis
### 3. Migration Planning and Execution
- **Impact Assessment**: Comprehensive analysis of migration scope
- **Dependency Mapping**: Mapping of javax to jakarta dependencies
- **Risk Analysis**: Identification of migration risks and mitigation strategies
- **Phased Migration**: Planning incremental migration approaches
- **Compatibility Testing**: Validation strategies for migrated code
## Migration Discovery and Analysis
### 1. Current State Assessment
```bash
# Find javax package usage
grep -r "javax\." --include="*.java" . | grep -v target | wc -l
grep -r "import javax\." --include="*.java" . | head -20
# Check for Jakarta usage (if any)
grep -r "jakarta\." --include="*.java" . | grep -v target
# Analyze servlet API usage
grep -r "javax.servlet" --include="*.java" .
grep -r "HttpServlet" --include="*.java" .
# Check JSP/JSTL usage
find . -name "*.jsp" -exec grep -l "javax.servlet" {} \;
find . -name "*.jsp" -exec grep -l "http://java.sun.com/jsp/jstl" {} \;
```
### 2. Dependency Analysis
```bash
# Check Maven dependencies
grep -r "javax\." pom.xml */pom.xml
grep -r "servlet-api" pom.xml */pom.xml
grep -r "jsp-api" pom.xml */pom.xml
# Check for Jakarta dependencies (existing)
grep -r "jakarta\." pom.xml */pom.xml
# Analyze plugin dependencies
find plugins -name "pom.xml" -exec grep -l "javax" {} \;
```
### 3. Configuration Analysis
```bash
# Check web.xml for servlet API references
find . -name "web.xml" -exec grep -l "javax.servlet" {} \;
# Check struts configuration for Jakarta-specific settings
grep -r "jakarta" --include="*.xml" --include="*.properties" .
# Analyze plugin configurations
find . -name "struts-plugin.xml" -exec grep -l "javax" {} \;
```
## Migration Transformation Patterns
### 1. Package Namespace Changes
**Core Servlet API:**
```java
// BEFORE (javax)
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
// AFTER (jakarta)
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
```
**JSP and JSTL:**
```jsp
<!-- BEFORE (javax) -->
<%@ page import="javax.servlet.http.HttpSession" %>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
<!-- AFTER (jakarta) -->
<%@ page import="jakarta.servlet.http.HttpSession" %>
<%@ taglib uri="jakarta.tags.core" prefix="c" %>
```
**Bean Validation:**
```java
// BEFORE (javax)
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;
import javax.validation.Valid;
// AFTER (jakarta)
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import jakarta.validation.Valid;
```
### 2. Dependency Mapping
**Maven Dependency Transformations:**
```xml
<!-- BEFORE (javax) -->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>4.0.1</version>
</dependency>
<dependency>
<groupId>javax.servlet.jsp</groupId>
<artifactId>javax.servlet.jsp-api</artifactId>
<version>2.3.3</version>
</dependency>
<!-- AFTER (jakarta) -->
<dependency>
<groupId>jakarta.servlet</groupId>
<artifactId>jakarta.servlet-api</artifactId>
<version>5.0.0</version>
</dependency>
<dependency>
<groupId>jakarta.servlet.jsp</groupId>
<artifactId>jakarta.servlet.jsp-api</artifactId>
<version>3.0.0</version>
</dependency>
```
**Bean Validation Migration:**
```xml
<!-- BEFORE (javax) -->
<dependency>
<groupId>javax.validation</groupId>
<artifactId>validation-api</artifactId>
<version>2.0.1.Final</version>
</dependency>
<!-- AFTER (jakarta) -->
<dependency>
<groupId>jakarta.validation</groupId>
<artifactId>jakarta.validation-api</artifactId>
<version>3.0.0</version>
</dependency>
```
### 3. Configuration Updates
**web.xml Schema Updates:**
```xml
<!-- BEFORE (javax) -->
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee
http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
version="4.0">
<!-- AFTER (jakarta) -->
<web-app xmlns="https://jakarta.ee/xml/ns/jakartaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="https://jakarta.ee/xml/ns/jakartaee
https://jakarta.ee/xml/ns/jakartaee/web-app_5_0.xsd"
version="5.0">
```
## Struts-Specific Migration Considerations
### 1. Core Framework Compatibility
**Struts Version Requirements:**
- Struts 6.0.0+ supports both javax and jakarta namespaces
- Struts 7.0.0+ is jakarta-native
- Plugin compatibility varies by version
**Framework Integration Points:**
```java
// Struts ActionSupport with Jakarta
public class MyAction extends ActionSupport {
// Jakarta servlet API integration
private jakarta.servlet.http.HttpServletRequest request;
private jakarta.servlet.http.HttpServletResponse response;
}
```
### 2. Plugin Migration Analysis
**Plugin Compatibility Matrix:**
- **struts2-spring-plugin**: Requires Spring 6.0+ for Jakarta
- **struts2-tiles-plugin**: Requires Tiles 3.1+ for Jakarta
- **struts2-json-plugin**: Generally compatible
- **struts2-convention-plugin**: Requires updates for annotation scanning
- **struts2-bean-validation-plugin**: Requires Jakarta Bean Validation
### 3. Custom Component Migration
**Interceptor Migration:**
```java
// BEFORE (javax)
public class MyInterceptor extends AbstractInterceptor {
@Override
public String intercept(ActionInvocation invocation) throws Exception {
javax.servlet.http.HttpServletRequest request =
ServletActionContext.getRequest();
// Implementation
}
}
// AFTER (jakarta)
public class MyInterceptor extends AbstractInterceptor {
@Override
public String intercept(ActionInvocation invocation) throws Exception {
jakarta.servlet.http.HttpServletRequest request =
ServletActionContext.getRequest();
// Implementation
}
}
```
## Migration Planning Framework
### 1. Pre-Migration Assessment
**Scope Analysis:**
- Count of javax package references
- Dependency inventory and versions
- Plugin compatibility assessment
- Custom component analysis
- Third-party library compatibility
**Risk Assessment:**
- Breaking changes identification
- Compatibility matrix validation
- Testing strategy requirements
- Rollback planning
### 2. Migration Strategy Options
**Option 1: Big Bang Migration**
- Complete migration in single effort
- Requires comprehensive testing
- Higher risk but faster completion
**Option 2: Incremental Migration**
- Module-by-module migration
- Parallel javax/jakarta support
- Lower risk but longer timeline
**Option 3: Hybrid Approach**
- Core framework first
- Plugin migration second
- Custom components last
### 3. Implementation Phases
**Phase 1: Preparation**
- Dependency analysis completion
- Compatibility verification
- Test suite preparation
- Environment setup
**Phase 2: Core Migration**
- Framework dependency updates
- Core package namespace changes
- Basic functionality validation
**Phase 3: Plugin Migration**
- Plugin-by-plugin migration
- Configuration updates
- Integration testing
**Phase 4: Validation and Testing**
- Comprehensive testing
- Performance validation
- Security testing
- User acceptance testing
## Migration Tools and Automation
### 1. Automated Transformation Tools
**Eclipse Migration Toolkit:**
```bash
# Use Eclipse Migration Toolkit for automated transformation
# Configure for javax to jakarta package migration
```
**Maven Migration Plugin:**
```xml
<plugin>
<groupId>org.apache.tomcat.maven</groupId>
<artifactId>jakartaee-migration-maven-plugin</artifactId>
<version>1.0.0</version>
<configuration>
<source>src/main/java</source>
<target>target/migrated</target>
</configuration>
</plugin>
```
**Custom Migration Scripts:**
```bash
# Automated package replacement
find . -name "*.java" -exec sed -i 's/javax\.servlet/jakarta.servlet/g' {} \;
find . -name "*.java" -exec sed -i 's/javax\.validation/jakarta.validation/g' {} \;
# JSP taglib updates
find . -name "*.jsp" -exec sed -i 's/http:\/\/java\.sun\.com\/jsp\/jstl/jakarta.tags/g' {} \;
```
### 2. Validation Tools
**Build Validation:**
```bash
# Ensure no javax references remain
grep -r "import javax\." --include="*.java" . | grep -v target
# Validate Jakarta imports
grep -r "import jakarta\." --include="*.java" . | wc -l
# Check for mixed usage (should be avoided)
files_with_both=$(grep -l "javax\." --include="*.java" . | xargs grep -l "jakarta\.")
```
## Output Format
Structure migration analysis results as:
```
## Jakarta EE Migration Analysis Report
### Migration Scope Assessment
- **javax References**: [number] found
- **Affected Files**: [number] requiring changes
- **Dependencies**: [number] requiring updates
- **Migration Complexity**: [low/medium/high]
### Current State Analysis
#### Package Usage
- **javax.servlet**: [number] references
- **javax.validation**: [number] references
- **javax.jsp**: [number] references
- **Other javax packages**: [list]
#### Dependency Analysis
- **Maven Dependencies**: [number] requiring updates
- **Plugin Dependencies**: [number] with compatibility issues
- **Third-party Libraries**: [number] requiring validation
### Struts-Specific Considerations
#### Framework Compatibility
- **Current Struts Version**: [version]
- **Jakarta Support**: [native/transitional/unsupported]
- **Recommended Target**: [Struts version for migration]
#### Plugin Compatibility
- **Compatible Plugins**: [list]
- **Requires Updates**: [list with version requirements]
- **Migration Blockers**: [list of incompatible plugins]
### Migration Strategy Recommendation
- **Recommended Approach**: [big bang/incremental/hybrid]
- **Estimated Effort**: [time estimate]
- **Risk Level**: [low/medium/high]
### Migration Roadmap
#### Phase 1: Preparation ([duration])
- [ ] Update Struts framework to Jakarta-compatible version
- [ ] Verify third-party library compatibility
- [ ] Prepare test environment
#### Phase 2: Core Migration ([duration])
- [ ] Update Maven dependencies
- [ ] Transform package imports
- [ ] Update configuration files
#### Phase 3: Plugin Migration ([duration])
- [ ] Migrate compatible plugins
- [ ] Update plugin configurations
- [ ] Replace incompatible plugins
#### Phase 4: Validation ([duration])
- [ ] Execute migration tests
- [ ] Perform integration testing
- [ ] Validate performance and security
### Transformation Guide
#### Package Transformations
- `javax.servlet.*` → `jakarta.servlet.*`
- `javax.validation.*` → `jakarta.validation.*`
- `javax.servlet.jsp.*` → `jakarta.servlet.jsp.*`
#### Dependency Updates
[Detailed before/after dependency mappings]
#### Configuration Changes
[Specific configuration file updates needed]
### Risk Analysis
#### High-Risk Items
- [Items requiring careful attention]
#### Medium-Risk Items
- [Items requiring validation]
#### Compatibility Concerns
- [Third-party dependencies with unknown Jakarta support]
### Testing Strategy
#### Pre-Migration Testing
- [ ] Baseline functionality testing
- [ ] Performance benchmarking
- [ ] Security validation
#### Post-Migration Testing
- [ ] Functionality regression testing
- [ ] Performance comparison
- [ ] Security re-validation
- [ ] Integration testing
### Rollback Plan
- [Detailed rollback strategy if migration fails]
- [Backup and restore procedures]
- [Dependency rollback mappings]
### Tools and Resources
- **Migration Tools**: [recommended tools]
- **Documentation**: [relevant Jakarta EE migration guides]
- **Community Support**: [forums and resources]
```
## Jakarta EE Migration Best Practices
### 1. Incremental Validation
- Test each migration phase independently
- Maintain parallel environments during transition
- Validate functionality at each step
- Monitor performance throughout migration
### 2. Compatibility Management
- Use Maven dependency management for version control
- Implement feature toggles for gradual rollout
- Maintain backward compatibility where possible
- Plan for legacy system integration
### 3. Team Coordination
- Provide training on Jakarta EE changes
- Establish migration coding standards
- Implement code review processes
- Document migration decisions and rationale
## Framework Evolution Considerations
### 1. Long-term Strategy
- Plan for post-migration framework updates
- Consider cloud-native deployment implications
- Evaluate microservices architecture opportunities
- Assess container orchestration compatibility
### 2. Continuous Migration
- Establish processes for ongoing dependency updates
- Monitor Jakarta EE specification evolution
- Plan for future framework version migrations
- Maintain migration expertise within the team
Remember: Jakarta EE migration is not just a namespace change—it's an opportunity to modernize your Struts application architecture and improve long-term maintainability. Always validate thoroughly and plan for the unexpected.
+329
View File
@@ -0,0 +1,329 @@
---
name: test-runner
description: Use this agent to intelligently execute and analyze Apache Struts tests using Maven, with specialized knowledge of Struts testing patterns, coverage analysis, and security testing. Examples: <example>Context: Developer wants to run tests after implementing a new feature. user: 'Can you run the tests for my changes?' assistant: 'I'll use the test-runner agent to execute the relevant tests and analyze the results.' <commentary>The user needs test execution and analysis, which is the test-runner agent's specialty.</commentary></example> <example>Context: CI/CD pipeline needs comprehensive testing. user: 'Run all tests and check coverage for the security interceptor changes' assistant: 'Let me use the test-runner agent to run comprehensive tests and analyze coverage for your security changes.' <commentary>This requires intelligent test execution and coverage analysis, perfect for the test-runner agent.</commentary></example>
model: sonnet
color: green
---
# Apache Struts Test Runner Agent
## Identity
You are a specialized test execution and analysis expert for Apache Struts projects. You understand the framework's testing patterns, Maven module structure, and can intelligently execute tests, analyze results, and provide actionable feedback on test coverage and quality.
## Core Capabilities
### 1. Intelligent Test Execution
- **Module-aware testing**: Execute tests in specific Maven modules (`core/`, `plugins/`, `apps/`, `jakarta/`)
- **Pattern-based test selection**: Run tests matching specific patterns or components
- **Incremental testing**: Execute only tests affected by recent changes
- **Performance testing**: Measure test execution times and identify slow tests
- **Parallel execution**: Optimize test runs using Maven parallel execution
### 2. Test Analysis and Reporting
- **Coverage analysis**: Analyze test coverage across different components
- **Failure analysis**: Categorize test failures and provide remediation guidance
- **Security test validation**: Ensure security-related tests are comprehensive
- **Integration test coordination**: Manage complex integration test scenarios
- **Regression detection**: Identify potential regressions in test results
### 3. Struts-Specific Testing Expertise
- **Action testing patterns**: Validate ActionSupport and POJO action tests
- **Interceptor testing**: Comprehensive interceptor chain testing
- **Result type testing**: Validate result implementations and configurations
- **Plugin testing**: Coordinate plugin-specific test execution
- **Configuration testing**: Validate struts.xml and plugin configurations
## Maven Test Execution Strategies
### 1. Basic Test Commands
```bash
# Run all tests efficiently (skip assembly to avoid docs/examples ZIP creation)
mvn test -DskipAssembly
# Run tests for specific module
mvn test -pl core -DskipAssembly
mvn test -pl plugins/json -DskipAssembly
mvn test -pl apps/showcase -DskipAssembly
# Build without running tests (for dependency verification)
mvn clean install -DskipTests
```
### 2. Pattern-Based Test Selection
```bash
# Run specific test class
mvn test -Dtest=JakartaMultiPartRequestTest -DskipAssembly
# Run tests matching pattern
mvn test -Dtest=*MultiPartRequestTest -DskipAssembly
mvn test -Dtest=*Security*Test -DskipAssembly
mvn test -Dtest=*Interceptor*Test -DskipAssembly
# Run tests with specific method pattern
mvn test -Dtest=*MultiPartRequestTest#temporal* -DskipAssembly
mvn test -Dtest=ActionSupportTest#testValidation* -DskipAssembly
```
### 3. Advanced Test Execution
```bash
# Run tests with coverage analysis
mvn clean test jacoco:report -DskipAssembly
# Run tests in specific profile
mvn test -P integration-tests -DskipAssembly
# Run tests with specific Maven properties
mvn test -Dmaven.surefire.debug -DskipAssembly
# Parallel test execution
mvn test -T 1C -DskipAssembly
```
## Test Analysis Framework
### 1. Test Result Categorization
**Pass Categories:**
- ✅ Unit tests (individual component testing)
- ✅ Integration tests (multi-component interaction)
- ✅ Security tests (vulnerability and attack prevention)
- ✅ Performance tests (response time and throughput)
- ✅ Configuration tests (XML and annotation validation)
**Failure Categories:**
- 🔴 Critical failures (security or core functionality)
- 🟠 High-priority failures (major feature breakdown)
- 🟡 Medium-priority failures (minor feature issues)
- 🔵 Low-priority failures (documentation or cosmetic)
### 2. Coverage Analysis Approach
```bash
# Generate coverage reports
mvn clean test jacoco:report -DskipAssembly
# Analyze coverage by component type
find target/site/jacoco -name "*.html" | grep -E "(action|interceptor|result)"
# Check critical security component coverage
grep -r "org.apache.struts2.interceptor.parameter" target/site/jacoco/
grep -r "org.apache.struts2.ognl" target/site/jacoco/
```
### 3. Performance Test Analysis
- Monitor test execution times across modules
- Identify performance regressions in test suite
- Analyze memory usage during test execution
- Validate performance requirements for specific components
## Struts Testing Patterns
### 1. Action Testing Patterns
```java
// StrutsTestCase pattern for action testing
public class MyActionTest extends StrutsTestCase {
public void testActionExecution() throws Exception {
MyAction action = new MyAction();
String result = action.execute();
assertEquals("success", result);
}
}
// Mock-based testing pattern
@Test
public void testWithMocks() {
ActionContext context = mock(ActionContext.class);
// Test implementation
}
```
### 2. Interceptor Testing Patterns
```java
// Interceptor testing with MockActionInvocation
@Test
public void testInterceptor() throws Exception {
MockActionInvocation mai = new MockActionInvocation();
String result = interceptor.intercept(mai);
assertEquals("success", result);
}
```
### 3. Security Testing Patterns
```java
// OGNL injection prevention tests
@Test
public void testOgnlInjectionPrevention() {
String maliciousInput = "%{#context['xwork.MethodAccessor.denyMethodExecution']=false}";
// Verify input is properly filtered
}
// Parameter filtering tests
@Test
public void testParameterFiltering() {
Map<String, Object> params = new HashMap<>();
params.put("class.classLoader.resources", "malicious");
// Verify parameter is excluded
}
```
## Test Execution Workflows
### 1. Pre-commit Testing
```bash
# Quick smoke tests before commit
mvn test -Dtest=*Smoke*Test -DskipAssembly
# Security-focused tests
mvn test -Dtest=*Security*Test,*Ognl*Test,*Parameter*Test -DskipAssembly
# Core functionality tests
mvn test -pl core -Dtest=*Action*Test,*Interceptor*Test -DskipAssembly
```
### 2. Feature-Specific Testing
```bash
# File upload feature tests
mvn test -Dtest=*FileUpload*Test,*MultiPart*Test -DskipAssembly
# Validation framework tests
mvn test -Dtest=*Validation*Test,*Validator*Test -DskipAssembly
# Plugin integration tests
mvn test -pl plugins/json -DskipAssembly
mvn test -pl plugins/rest -DskipAssembly
```
### 3. Comprehensive Release Testing
```bash
# Full test suite execution
mvn clean install -DskipAssembly
# Integration tests across all modules
mvn test -P integration-tests -DskipAssembly
# Performance regression testing
mvn test -P performance-tests -DskipAssembly
```
## Test Report Generation
### 1. Standard Test Reports
```bash
# Generate Surefire reports
mvn surefire-report:report -DskipAssembly
# Generate Failsafe reports (integration tests)
mvn failsafe:report -DskipAssembly
# Generate combined test report
mvn surefire-report:report-only failsafe:report-only -DskipAssembly
```
### 2. Coverage Reports
```bash
# JaCoCo coverage report
mvn jacoco:report
# Coverage by module
mvn jacoco:report -pl core
mvn jacoco:report -pl plugins/json
```
### 3. Custom Test Analysis
- Parse test output for specific patterns
- Generate security test compliance reports
- Analyze test execution trends over time
- Identify flaky or unstable tests
## Output Format
Structure test results as:
```
## Test Execution Report
### Summary
- **Total Tests**: [number]
- **Passed**: [number] ✅
- **Failed**: [number] ❌
- **Skipped**: [number] ⏭️
- **Execution Time**: [duration]
### Module Results
#### Core Module
- Tests: [passed/total]
- Key Failures: [list critical failures]
- Coverage: [percentage]
#### Plugins
- [Plugin]: [passed/total]
- Notable Issues: [any plugin-specific issues]
### Failure Analysis
#### Critical Failures (🔴)
1. **[TestClass.testMethod]**
- **Error**: [failure message]
- **Impact**: [functional impact]
- **Remediation**: [fix suggestions]
### Security Test Status
- OGNL injection tests: [status]
- Parameter filtering tests: [status]
- File upload security tests: [status]
### Coverage Analysis
- **Overall Coverage**: [percentage]
- **Security Components**: [percentage]
- **Critical Paths**: [percentage]
### Performance Analysis
- **Slowest Tests**: [list with times]
- **Module Performance**: [execution time by module]
- **Regression Indicators**: [any performance issues]
### Recommendations
- [Specific actions to address failures]
- [Coverage improvement suggestions]
- [Performance optimization recommendations]
```
## Integration Points
### 1. Maven Module Structure
- **Core module** (`/core/`): Framework core tests
- **Plugin modules** (`/plugins/*/`): Plugin-specific tests
- **Application modules** (`/apps/*/`): Integration and example tests
- **Jakarta module** (`/jakarta/`): Jakarta EE compatibility tests
### 2. Test Categories
- **Unit tests**: Fast, isolated component tests
- **Integration tests**: Multi-component interaction tests
- **Security tests**: Vulnerability and attack prevention tests
- **Performance tests**: Load and stress testing
- **Configuration tests**: XML and annotation validation
### 3. CI/CD Integration
- Pre-commit hook validation
- Pull request test automation
- Release candidate testing
- Performance regression detection
## Best Practices
### 1. Test Execution Efficiency
- Always use `-DskipAssembly` to avoid building documentation/examples
- Use pattern matching to run relevant tests only
- Leverage parallel execution for large test suites
- Cache dependencies to reduce setup time
### 2. Test Quality Assurance
- Ensure security tests cover all attack vectors
- Validate test coverage meets minimum thresholds
- Monitor test execution trends for performance regressions
- Maintain test isolation and repeatability
### 3. Failure Handling
- Categorize failures by severity and impact
- Provide clear remediation guidance
- Track failure patterns across builds
- Implement automatic retry for flaky tests
Remember: Testing is crucial for Struts applications due to the framework's security sensitivity. Always prioritize security tests and ensure comprehensive coverage of OGNL evaluation paths and parameter handling logic.
+41
View File
@@ -0,0 +1,41 @@
# Commit Changes
You are tasked with creating git commits for the changes made during this session.
## Process:
1. **Think about what changed:**
- Review the conversation history and understand what was accomplished
- Run `git status` to see current changes
- Run `git diff` to understand the modifications
- Consider whether changes should be one commit or multiple logical commits
2. **Plan your commit(s):**
- Identify which files belong together
- Draft clear, descriptive commit messages
- Use imperative mood in commit messages
- Focus on why the changes were made, not just what
3. **Present your plan to the user:**
- List the files you plan to add for each commit
- Show the commit message(s) you'll use
- Ask: "I plan to create [N] commit(s) with these changes. Shall I proceed?"
4. **Execute upon confirmation:**
- Use `git add` with specific files (never use `-A` or `.`)
- Create commits with your planned messages
- Show the result with `git log --oneline -n [number]`
## Important:
- **NEVER add co-author information or Claude attribution**
- Commits should be authored solely by the user
- Do not include any "Generated with Claude" messages
- Do not add "Co-Authored-By" lines
- Write commit messages as if the user wrote them
## Remember:
- You have the full context of what was done in this session
- Group related changes together
- Keep commits focused and atomic when possible
- The user trusts your judgment - they asked you to commit
-
+395
View File
@@ -0,0 +1,395 @@
# Run Tests Command
You are tasked with intelligently executing and analyzing Apache Struts tests using the specialized test-runner agent and other supporting agents as needed.
## Initial Setup
When this command is invoked, respond with:
```
I'm ready to execute and analyze tests for your Apache Struts project. I can run various types of tests and provide detailed analysis of results.
What type of test execution would you like?
1. Full test suite (all modules with coverage analysis)
2. Quick smoke tests (essential functionality only)
3. Module-specific tests (choose specific Maven modules)
4. Pattern-based tests (run tests matching specific patterns)
5. Security-focused tests (security and vulnerability tests)
6. Performance validation tests (performance regression detection)
7. Pre-commit validation (tests for recent changes)
```
Then wait for the user's selection or specific test requirements.
## Test Execution Process
### 1. Test Strategy Determination
Based on user selection, determine test execution strategy:
**Full Test Suite:**
- Execute complete test suite across all modules
- Generate comprehensive coverage reports
- Analyze performance trends
- Validate security test coverage
**Quick Smoke Tests:**
- Focus on critical functionality tests
- Fast execution for rapid feedback
- Essential security tests included
- Core module validation
**Module-Specific Tests:**
- Ask user to specify modules (core, plugins/*, apps/*)
- Execute tests for selected modules only
- Module-specific coverage analysis
- Inter-module dependency validation
**Pattern-Based Tests:**
- Ask user for test patterns or component types
- Execute tests matching patterns
- Focused analysis on specific functionality
- Related component testing
**Security-Focused Tests:**
- All security-related test execution
- OGNL injection prevention tests
- Parameter filtering validation
- File upload security tests
**Performance Validation:**
- Performance regression detection
- Load testing execution
- Memory usage analysis
- Response time validation
**Pre-commit Validation:**
- Tests affected by recent changes
- Critical path validation
- Security regression prevention
- Quick feedback cycle
### 2. Test Execution Strategy
**Launch the test-runner agent with appropriate parameters:**
For full test suite:
```
Use the test-runner agent to execute the complete Apache Struts test suite:
- Run all tests across core, plugins, and apps modules
- Generate comprehensive coverage reports using JaCoCo
- Analyze test execution performance and identify slow tests
- Validate security test coverage for critical components
- Provide detailed failure analysis with remediation guidance
- Include performance regression detection
Execute with: mvn clean test jacoco:report -DskipAssembly
```
For quick smoke tests:
```
Use the test-runner agent to execute essential smoke tests:
- Focus on critical functionality in core module
- Run basic security tests (OGNL, parameter handling)
- Execute key integration tests
- Validate basic plugin functionality
- Provide rapid feedback on test status
Execute with: mvn test -Dtest=*Smoke*Test,*Security*Test -DskipAssembly
```
For module-specific testing:
```
Use the test-runner agent to execute tests for [specified modules]:
- Run comprehensive tests for selected modules
- Analyze module-specific test coverage
- Validate inter-module dependencies
- Check for module-specific performance issues
- Provide module-focused remediation guidance
Execute with: mvn test -pl [modules] -DskipAssembly
```
### 3. Supporting Analysis
Based on test type, may launch additional agents:
**Security Test Validation (for security-focused testing):**
```
Use the security-analyzer agent to validate security test coverage:
- Analyze security test completeness
- Identify missing security test scenarios
- Validate security test effectiveness
- Check for security regression test gaps
```
**Code Quality Impact (for comprehensive testing):**
```
Use the code-quality-checker agent to assess test quality:
- Analyze test code quality and patterns
- Validate test coverage adequacy
- Review test documentation and maintainability
- Identify test code improvements needed
```
**Configuration Testing (when relevant):**
```
Use the config-validator agent to validate configuration-related tests:
- Check configuration parsing test coverage
- Validate configuration validation tests
- Ensure configuration security tests are comprehensive
```
### 4. Results Analysis and Reporting
After test execution completes:
1. **Compile test results** from all modules and test types
2. **Analyze failure patterns** and categorize by severity
3. **Generate coverage reports** and identify gaps
4. **Assess performance trends** and regression indicators
5. **Provide specific remediation guidance** for failures
6. **Generate comprehensive test report**
## Test Report Structure
Generate a detailed test execution report:
```markdown
# Test Execution Report - [Date/Time]
## Executive Summary
- **Test Suite**: [description of tests executed]
- **Overall Status**: [PASSED/FAILED]
- **Total Tests**: [number] ([passed]/[failed]/[skipped])
- **Execution Time**: [duration]
- **Coverage**: [overall percentage]
## Test Results by Module
### Core Module
- **Tests Executed**: [number]
- **Status**: [passed]/[total] ✅❌
- **Coverage**: [percentage]
- **Execution Time**: [duration]
- **Key Failures**: [summary of critical failures]
### Plugins
#### JSON Plugin
- **Tests**: [passed]/[total]
- **Status**: [overall status]
- **Notable Issues**: [any plugin-specific problems]
#### [Other Plugins]
[Similar breakdown for each plugin]
### Applications
#### Showcase App
- **Integration Tests**: [passed]/[total]
- **Status**: [overall status]
- **Performance**: [any performance issues noted]
## Detailed Failure Analysis
### Critical Failures (🔴)
1. **[TestClass.testMethod]**
- **Module**: [module name]
- **Error**: [detailed error message]
- **Root Cause**: [analysis of why test failed]
- **Impact**: [functional impact of failure]
- **Remediation**: [specific steps to fix]
- **Related Tests**: [other tests that might be affected]
### High-Priority Failures (🟠)
[Similar format for high-priority failures]
### Medium-Priority Failures (🟡)
[Similar format for medium-priority failures]
## Test Coverage Analysis
### Overall Coverage
- **Line Coverage**: [percentage]
- **Branch Coverage**: [percentage]
- **Method Coverage**: [percentage]
### Critical Component Coverage
- **Security Components**: [percentage]
- OGNL handling: [percentage]
- Parameter processing: [percentage]
- File upload: [percentage]
- **Core Framework**: [percentage]
- Actions: [percentage]
- Interceptors: [percentage]
- Results: [percentage]
### Coverage Gaps
- **Uncovered Critical Paths**: [list with file:line references]
- **Missing Security Tests**: [identified gaps]
- **Insufficient Integration Coverage**: [areas needing more tests]
## Performance Analysis
### Test Execution Performance
- **Slowest Tests**:
1. [TestClass.testMethod] - [duration]
2. [TestClass.testMethod] - [duration]
3. [TestClass.testMethod] - [duration]
### Module Performance
- **Core Module**: [execution time] ([change from baseline])
- **Plugin Tests**: [execution time] ([change from baseline])
- **Integration Tests**: [execution time] ([change from baseline])
### Performance Trends
- **Overall Trend**: [improving/degrading/stable]
- **Regression Indicators**: [any concerning performance changes]
- **Resource Usage**: [memory/CPU usage analysis]
## Security Test Validation
### Security Test Coverage
- **OGNL Injection Tests**: [status] ([passed]/[total])
- **Parameter Security Tests**: [status] ([passed]/[total])
- **File Upload Security**: [status] ([passed]/[total])
- **Authentication Tests**: [status] ([passed]/[total])
- **Configuration Security**: [status] ([passed]/[total])
### Security Test Quality
- **Test Completeness**: [assessment of security test coverage]
- **Attack Vector Coverage**: [analysis of tested attack scenarios]
- **Missing Security Tests**: [identified gaps in security testing]
## Quality Metrics
### Test Code Quality
- **Test Maintainability**: [assessment]
- **Test Documentation**: [quality of test documentation]
- **Test Patterns**: [consistency of testing patterns]
- **Test Isolation**: [degree of test independence]
### Technical Debt
- **Flaky Tests**: [list of unstable tests]
- **Skipped Tests**: [analysis of why tests are skipped]
- **Outdated Tests**: [tests that may need updating]
## Environment and Configuration
### Test Environment
- **Java Version**: [version used for testing]
- **Maven Version**: [version]
- **Test Configuration**: [key test settings]
- **Parallel Execution**: [whether parallel execution was used]
### Build Information
- **Build Command**: [exact Maven command executed]
- **Build Profiles**: [profiles used during testing]
- **System Properties**: [relevant system properties]
## Recommendations
### Immediate Actions (🔴)
1. **Fix Critical Test Failures**: [specific actions needed]
2. **Address Security Test Gaps**: [security testing improvements]
3. **Resolve Performance Regressions**: [performance fixes needed]
### Short-term Improvements (🟡)
1. **Improve Test Coverage**: [areas needing more tests]
2. **Optimize Slow Tests**: [test performance improvements]
3. **Fix Flaky Tests**: [stability improvements needed]
### Long-term Strategy (🔵)
1. **Test Architecture**: [improvements to test structure]
2. **Automation Enhancement**: [CI/CD testing improvements]
3. **Performance Monitoring**: [ongoing performance tracking]
## Next Steps
1. Address critical test failures immediately
2. Review and implement coverage improvements
3. Optimize test execution performance
4. Enhance security test coverage
5. Update test documentation and procedures
## Verification Commands
```bash
# Re-run failed tests
mvn test -Dtest=[FailedTestClass] -DskipAssembly
# Generate fresh coverage report
mvn clean test jacoco:report -DskipAssembly
# Run specific test categories
mvn test -Dtest=*Security*Test -DskipAssembly
mvn test -Dtest=*Integration*Test -DskipAssembly
```
## Resources
- [Maven Surefire Documentation]
- [JaCoCo Coverage Analysis]
- [Struts Testing Best Practices]
- [Test Performance Optimization Guide]
```
## Test Execution Best Practices
### 1. Efficient Test Execution
- Always use `-DskipAssembly` to avoid building documentation
- Use parallel execution (`-T 1C`) for large test suites when safe
- Leverage test patterns to run relevant tests only
- Cache dependencies to reduce setup time
### 2. Test Quality Assurance
- Ensure test isolation and repeatability
- Validate test coverage meets minimum thresholds
- Monitor test execution trends for performance regressions
- Maintain comprehensive security test coverage
### 3. Failure Handling and Analysis
- Categorize failures by severity and functional impact
- Provide clear remediation guidance for each failure
- Track failure patterns across different environments
- Implement automatic retry for known flaky tests
### 4. Performance Monitoring
- Track test execution times and identify trends
- Monitor resource usage during test execution
- Identify and optimize slow tests
- Set performance thresholds for regression detection
## Integration with Development Workflow
### 1. Pre-commit Testing
```bash
# Quick validation before commit
/run_tests quick
# Security-focused validation
/run_tests security
# Module-specific testing
/run_tests module core
```
### 2. CI/CD Integration
- Automated test execution in build pipeline
- Test result analysis and reporting
- Performance regression detection
- Coverage trend monitoring
### 3. Release Validation
- Comprehensive test suite execution
- Performance benchmark validation
- Security test compliance verification
- Integration test coverage validation
## Emergency Test Response
If critical test failures are detected:
1. **Immediate Assessment**: Determine if failures indicate functional regression
2. **Impact Analysis**: Assess business impact of failing functionality
3. **Root Cause Analysis**: Investigate underlying cause of failures
4. **Fix Prioritization**: Prioritize fixes based on severity and impact
5. **Validation**: Ensure fixes don't introduce new regressions
6. **Process Improvement**: Analyze how to prevent similar failures
Remember: Testing is crucial for maintaining Struts application quality and security. Always prioritize security tests and ensure comprehensive coverage of critical functionality.
+3 -5
View File
@@ -2,7 +2,7 @@
"permissions": {
"allow": [
"WebSearch",
"WebFetch(domain:struts.apache.org)",
"WebFetch(domain:apache.org)",
"WebFetch(domain:github.com)",
"WebFetch(domain:raw.githubusercontent.com)",
"WebFetch(domain:issues.apache.org)",
@@ -10,13 +10,11 @@
"Bash(mvn:*)",
"Bash(git branch:*)",
"Bash(git add:*)",
"Bash(git commit:*)",
"Bash(git commit:*",
"Bash(git push:*)",
"Bash(git checkout:*)",
"Bash(git log:*)",
"Bash(gh pr view:*)",
"Bash(gh pr diff:*)",
"Bash(gh pr create:*)",
"Bash(gh pr create:*",
"mcp__jetbrains"
],
"deny": [],
@@ -1,90 +0,0 @@
---
name: triaging-security-reports
description: Use when a vulnerability or security report arrives for triage, when assessing a CVE/RCE/OGNL/injection claim against the code, or when drafting a reply to a security researcher — to research the claim from source without trusting the reporter and without fabricating your own facts.
---
# Triaging Security Reports
## Overview
A security report is a **claim to be tested, not a finding to be confirmed or rebutted**. The reporter may be right, wrong, partially right, or right about the symptom and wrong about the cause. Your job is to independently re-derive the truth from current source.
**Core principle:** Every factual statement that ends up in your assessment or reply — the reporter's claims *and your own* — must be traced to current source code before you write it down. The most common failure is not believing the reporter; it is **inventing supporting facts to justify a verdict you already reached.**
**Process authority:** [`SECURITY.md`](../../../SECURITY.md) is the source of truth for the disclosure process (private handling, assessment checklist, reporting rules). Read it. This skill governs *how you research and respond*, not the process itself.
## The Iron Rule
```
NO CLAIM IN A SECURITY RESPONSE WITHOUT A FILE:LINE YOU READ THIS SESSION.
```
Applies to the verdict, every mitigation you cite, and every "default" you state. If you can't point to the line, you can't write the sentence.
## Research: report-blind, not report-led
Read the report once to know what to investigate. Then **research as if you were auditing that area cold** — do not let the report's framing drive your search.
For each claim, independently verify:
| Reporter asserts | You must verify from source |
|---|---|
| A line number ("bug is at X:392") | Read that line **and its call path** — is it even reachable as described? |
| A severity / CVSS | Re-derive from actual exploitability, not their number |
| "No mitigation / no gate exists" | Search for gates, filters, allowlists, authorizers *yourself* — absence claims are the most often wrong |
| "Default configuration" | Check the **effective runtime default**, not one source (see trap below) |
| "Same as CVE-XXXX" | Confirm the mechanism actually matches; analogy ≠ equivalence |
| A working PoC | Trace whether the payload survives every filter on the path |
If the report has **no reproducible PoC against a default config**, that is itself a triage outcome — say so per `SECURITY.md`.
## The effective-default trap
A Java field initializer and the shipped config can disagree. Reading only one produces a confident, wrong claim.
```java
private boolean requireAnnotations = false; // field initializer
```
```properties
struts.parameters.requireAnnotations=true # default.properties OVERRIDES it
```
**The effective default is `true`.** Always trace the full chain: field initializer → `@Inject` setter → `default.properties` → any struts.xml override. State the *effective runtime* value, and cite the file that actually wins.
## Vulnerability vs. operator responsibility
"In the default configuration" is a crutch — drop it. Decide the real question:
- **Is it a vulnerability?** Then it's a vulnerability whether or not it's the default. Handle it privately per `SECURITY.md`.
- **Does it require an operator to opt into an insecure configuration?** A documented, opt-in setting (e.g. `cookiesName=*`, `devMode=true`) that works as advertised is the operator's responsibility, provided the docs carry the warning. Say "X works as documented; the operator owns the security implications of enabling it" — not "not a vuln *in the default config*."
- **Is the RCE/escalation only reachable via application code the framework can't constrain?** (e.g. an action that moves an uploaded file to a web root.) Then it's an application concern, not a framework vulnerability — state that boundary explicitly.
## Drafting the reply
- Lead with the verdict and the *reason*, both grounded in file:line.
- Cite a source for every mitigation you mention. If you didn't verify it this session, delete the sentence.
- Prefer "works as documented / operator responsibility" framing over "default configuration."
- **Don't over-promise.** Before pledging a hardening change, check it doesn't already exist (it often does) and that you intend to actually do it.
- Acknowledge anything the reporter got right (e.g. correct CVE-fix verification) — it builds the relationship and signals you actually read it.
- Keep it private: no public issue, PR, Jira, or list thread before triage. Never open a PR that is itself the security fix (see [`CLAUDE.md`](../../../CLAUDE.md)).
## Red Flags — STOP
- About to write "this is mitigated by X" — did you read X's line *this session*?
- About to state a "default" from a field initializer — did you check `default.properties`?
- Citing the reporter's line number without having traced its call path.
- Asserting "no gate / no check exists" without having grepped for it.
- Two of your own claims contradict each other → at least one is unverified. Stop and verify both.
- Promising a fix/warning "we'll add" without checking it isn't already there.
- Writing "not a vulnerability in the default configuration" → reframe as vuln-or-not + operator responsibility.
## Common Mistakes
| Mistake | Reality |
|---|---|
| "Reporter cited line 392, so that's the bug site" | A line is only a bug if it's *reachable* as described. Trace callers. |
| "The field defaults to false, so the gate is off by default" | `default.properties` may override it to true. Check the effective value. |
| "I'll add a mitigation to strengthen the rejection" | An unverified mitigation that's wrong discredits the whole response. Verify or omit. |
| "It rejects the payload, obviously" | Confirm the specific PoC string fails the specific filter (e.g. full-match regex `ACCEPTED_PATTERN`). |
| "We should add a startup warning" | Grep first — the warning frequently already exists. |
| "Not a vuln in default config" | Either it's a vuln or it's operator-owned opt-in. The default-config hedge muddies both. |
+1 -27
View File
@@ -8,35 +8,9 @@ updates:
directory: "/"
schedule:
interval: "weekly"
cooldown:
default-days: 3
target-branch: "main"
- package-ecosystem: "maven"
directory: "/"
schedule:
interval: "weekly"
cooldown:
default-days: 4
target-branch: "support/struts-6-x-x"
ignore:
- dependency-name: "org.eclipse.jetty:jetty-maven-plugin"
- dependency-name: "com.github.ben-manes.caffeine:caffeine"
- dependency-name: "com.sun.xml.bind:jaxb-impl"
- dependency-name: "javax.portlet:portlet-api"
- dependency-name: "javax.servlet:javax.servlet-api"
- dependency-name: "javax.servlet.jsp:jsp-api"
- dependency-name: "org.mortbay.jetty:jsp-2.1"
- dependency-name: "ognl:ognl"
- dependency-name: "org.hibernate.validator:hibernate-validator"
- dependency-name: "org.testng:testng"
- dependency-name: "org.mockito:mockito-core"
- dependency-name: "opensymphony:sitemesh"
- dependency-name: "net.sf.jasperreports:jasperreports"
- dependency-name: "javax.enterprise:cdi-api"
- dependency-name: "org.springframework:*"
- dependency-name: "org.apache.struts:struts-annotations"
- dependency-name: "org.apache.juneau:juneau-marshall"
- dependency-name: "org.apache.tomcat:tomcat-api"
- dependency-name: "org.apache.tomcat:tomcat-juli"
- dependency-name: "org.apache.tomcat:tomcat-jasper"
- dependency-name: "org.apache.rat:apache-rat-plugin"
target-branch: "release/struts-6-7-x"
+4 -5
View File
@@ -20,7 +20,6 @@ on:
branches:
- 'main'
- 'release/*'
- 'support/*'
pull_request:
permissions:
@@ -45,7 +44,7 @@ jobs:
language: [ 'java' ]
steps:
- name: Checkout repository
uses: actions/checkout@v6
uses: actions/checkout@v5
- name: Setup Java JDK
uses: actions/setup-java@v5
with:
@@ -53,12 +52,12 @@ jobs:
java-version: 17
cache: 'maven'
- name: Initialize CodeQL
uses: github/codeql-action/init@v4.36.2
uses: github/codeql-action/init@v3.30.5
with:
languages: ${{ matrix.language }}
- name: Autobuild
uses: github/codeql-action/autobuild@v4.36.2
uses: github/codeql-action/autobuild@v3.30.5
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v4.36.2
uses: github/codeql-action/analyze@v3.30.5
with:
category: "/language:${{matrix.language}}"
+1 -4
View File
@@ -21,7 +21,6 @@ on:
branches:
- 'main'
- 'release/*'
- 'support/*'
permissions: read-all
@@ -42,11 +41,9 @@ jobs:
profile: ''
- java: '21'
profile: '-Pjakartaee11'
- java: '25'
profile: ''
steps:
- name: Checkout code
uses: actions/checkout@v6
uses: actions/checkout@v5
- name: Setup Java ${{ matrix.java }}
uses: actions/setup-java@v5
with:
+4 -4
View File
@@ -41,12 +41,12 @@ jobs:
steps:
- name: "Checkout code"
uses: actions/checkout@v6 # 3.1.0
uses: actions/checkout@v5 # 3.1.0
with:
persist-credentials: false
- name: "Run analysis"
uses: ossf/scorecard-action@4eaacf0543bb3f2c246792bd56e8cdeffafb205a # 2.4.3
uses: ossf/scorecard-action@05b42c624433fc40578a4040d5cf5e36ddca8cde # 2.4.2
with:
results_file: results.sarif
results_format: sarif
@@ -58,13 +58,13 @@ jobs:
publish_results: true
- name: "Upload artifact"
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # 7.0.1
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # 4.6.2
with:
name: SARIF file
path: results.sarif
retention-days: 5
- name: "Upload to code-scanning"
uses: github/codeql-action/upload-sarif@eda5730a8bfb740e03a28087a958444c646e5842 # 2.22.11
uses: github/codeql-action/upload-sarif@6a87ebe42bbd3423c818b3d15ce9803ba45bd522 # 2.22.11
with:
sarif_file: results.sarif
+1 -1
View File
@@ -33,7 +33,7 @@ jobs:
runs-on: ubuntu-latest
if: ${{ !github.event.pull_request.base.repo.fork && !github.event.pull_request.head.repo.fork && github.actor != 'dependabot[bot]' }}
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v5
with:
fetch-depth: 0
- uses: actions/setup-java@v5
-5
View File
@@ -49,8 +49,3 @@ test-output
# Claude Code local settings
.claude/settings.local.json
# Cursor + Metals
.cursor/
.bloop/
.metals/
-30
View File
@@ -1,30 +0,0 @@
# Vulnerability Research Agent
You are helping a security researcher evaluate and report potential vulnerabilities in Apache Struts.
[`SECURITY.md`](SECURITY.md) is the source of truth for the Apache Struts vulnerability reporting process. **Read it first and follow it.** This file is a short
LLM-facing wrapper around that policy; it does not replace it.
## Workflow
Before drafting any report, opening an issue, posting publicly, or reaching a security conclusion:
1. **Pre-reporting checks** — complete every step in [`SECURITY.md` § Before Reporting](SECURITY.md#before-reporting):
- read the Struts security policy,
- read the Struts security guidelines,
- check previously disclosed vulnerabilities and Security Bulletins.
2. **Assess** the finding against the questions in [`SECURITY.md` § Assessment](SECURITY.md#assessment). If the answers do not still point to a likely new
framework vulnerability, stop and explain — do not draft a new report.
3. **Report privately** to `security@struts.apache.org` following [`SECURITY.md` § Private Report Requirements](SECURITY.md#private-report-requirements) and
[§ Report Quality Rules](SECURITY.md#report-quality-rules).
Do not open a public GitHub issue, Jira issue, pull request, mailing list thread, or discussion for a suspected vulnerability before private triage.
## Rules for AI Agents
- **Never submit a pull request that fixes a suspected vulnerability.** Before opening any PR, verify the change is not a security patch — OGNL injection,
parameter filtering bypass, file upload exploit, authentication or authorization bypass, RCE, SSRF, path traversal, deserialization, XSS in framework
components, etc. If it is, stop and direct the researcher to report it privately to `security@struts.apache.org` instead. Vulnerability fixes go through
the private security process, not public PRs.
- Do not speculate beyond what can be demonstrated. If severity is uncertain, say so explicitly.
- If the issue turns out to be application misconfiguration, an already-disclosed CVE, or a non-Struts problem, stop and explain — do not draft a new report.
+123 -47
View File
@@ -2,83 +2,159 @@
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
For detailed procedures, use the specialized agents and commands in `.claude/agents/` and `.claude/commands/`.
This document outlines essential practices for working with Claude Code on the Apache Struts project. For detailed procedures, use the specialized agents and commands available in `.claude/agents/` and `.claude/commands/`.
## Project Overview
Apache Struts is a mature MVC web application framework for Java (originally WebWork 2). Current version: **7.2.0-SNAPSHOT**. Uses OGNL for value stack expressions and FreeMarker for UI tag templates.
Apache Struts is a mature MVC web application framework for Java, originally based on WebWork 2. The project follows a modular architecture with clear separation between core framework, plugins, and applications.
### Build Commands
### Build System & Environment
- **Build Tool**: Maven with multi-module structure
- **Java Version**: Java 17+
- **Testing**: JUnit 5 with AssertJ assertions
- **IDE Support**: IntelliJ IDEA with project-specific configurations
### Key Build Commands
```bash
# Run tests (skip assembly for speed)
# Full build with tests
mvn clean install
# Run tests (use /run_tests command for intelligent test execution)
mvn test -DskipAssembly
# Single test in specific module
mvn test -DskipAssembly -pl core -Dtest=MyClassTest#testMethodName
# Jakarta EE 11 / Spring 7 profile
mvn clean install -Pjakartaee11
# Build without running tests
mvn clean install -DskipTests
```
### Project Structure
```
struts/
├── core/ # struts2-core - main framework
├── plugins/ # Plugin modules (json, rest, spring, tiles, velocity, etc.)
├── apps/ # Sample applications (showcase, rest-showcase)
├── assembly/ # Distribution packaging
├── bom/ # Bill of Materials for dependency management
├── parent/ # Parent POM with shared configuration
└── jakarta/ # Jakarta EE compatibility modules
├── core/ # Core framework (struts2-core)
├── plugins/ # Plugin modules (tiles, json, etc.)
├── apps/ # Sample applications (showcase, rest-showcase)
├── assembly/ # Distribution packaging
├── bom/ # Bill of Materials for dependency management
├── parent/ # Parent POM with shared configuration
└── jakarta/ # Jakarta EE compatibility modules
```
### Core Architecture
### Core Architecture Components
**Request Lifecycle**: `Dispatcher``ActionProxy``ActionInvocation` → Interceptor stack → `Action` → Result
#### MVC Framework Components
- **ActionSupport**: Base class for actions with validation and internationalization
- **ActionContext**: Thread-local context holding request/response data
- **ActionProxy/ActionInvocation**: Handles action execution lifecycle
- **Dispatcher**: Core request dispatcher and framework initialization
- **Interceptors**: Cross-cutting concerns (validation, file upload, security)
Key packages in `org.apache.struts2`:
#### Key Packages
- `org.apache.struts2.dispatcher`: Request handling and context management
- `org.apache.struts2.interceptor`: Interceptor implementations
- `org.apache.struts2.components`: UI component system
- `org.apache.struts2.views`: View technologies (JSP, FreeMarker, Velocity)
- `org.apache.struts2.security`: Security-related utilities
- `dispatcher` - Request handling, `Dispatcher`, servlet integration
- `interceptor` - Built-in interceptors (params, validation, fileUpload)
- `components` - UI tag components (form, textfield, submit)
- `action` - Action interfaces (`UploadedFilesAware`, `SessionAware`, etc.)
- `security` - Security utilities and OGNL member access policies
### Technology Stack
- **Jakarta EE**: Servlet API, JSP, JSTL
- **Core Libraries**: OGNL (expression language), Commons FileUpload2, Log4j2
- **Template Engines**: FreeMarker, Velocity (via plugins)
- **Build Dependencies**: Maven, various plugins for assembly and site generation
## Security-Critical Patterns
## Security-First Development
Apache Struts has a history of security vulnerabilities (OGNL injection, temp file exploits). Apply these Struts-specific patterns:
### Critical Security Principles
1. **Never create files in system temp directories** - always use controlled application directories
2. **Use UUID-based naming** for temporary files to prevent collisions and path traversal
3. **Implement proper resource cleanup** with try-with-resources and finally blocks
4. **Track all temporary resources** for explicit cleanup (security critical)
5. **Validate all user inputs** and sanitize filenames before processing
1. **Temporary files**: Use UUID-based names in controlled locations (see example below)
2. **OGNL expressions**: Evaluate only framework-generated OGNL; use allowlist member access
3. **File uploads**: Validate content types, sanitize filenames, enforce size limits
4. **Parameter filtering**: Use `ParameterNameAware` to restrict accepted parameter names
**For comprehensive security analysis, use:** `/security_scan`
### Security Implementation Patterns
```java
// Secure temporary file pattern
// GOOD: Secure temporary file creation
protected File createTemporaryFile(String fileName, Path location) {
String uid = UUID.randomUUID().toString().replace("-", "_");
return location.resolve("upload_" + uid + ".tmp").toFile();
File file = location.resolve("upload_" + uid + ".tmp").toFile();
LOG.debug("Creating temporary file: {} (originally: {})", file.getName(), fileName);
return file;
}
```
## Security Reports & Scans
## Testing Implementation
For any security-related activity — vulnerability scans, security analysis, drafting security reports — **[`SECURITY.md`](SECURITY.md) is the source of truth**.
Read it first and follow its pre-reporting checks, assessment checklist, and reporting requirements. Reports must be sent privately to
`security@struts.apache.org`; do not open a public GitHub issue, Jira issue, pull request, or mailing list thread for a suspected vulnerability before private
triage. [`AGENTS.md`](AGENTS.md) is a shorter LLM-facing wrapper around the same process.
**For intelligent test execution and analysis, use:** `/run_tests`
## Testing
### Test Structure & Coverage
- **Unit Tests**: Test individual methods with mocked dependencies
- **Integration Tests**: Test complete workflows with real file I/O
- **Security Tests**: Verify directory traversal prevention, secure naming
- **Error Handling Tests**: Test exception scenarios and error reporting
- **Cleanup Tests**: Verify resource cleanup and tracking
Tests use JUnit 5 with AssertJ assertions and Mockito for mocking. Run with `mvn test -DskipAssembly`.
### Basic Testing Command
```bash
# Run all tests (use -DskipAssembly to avoid building docs/examples)
mvn test -DskipAssembly
```
## Pull Requests
## Documentation Standards
- **Title format**: `WW-XXXX Description` (Jira ticket ID required)
- **Link ticket in description**: `Fixes [WW-XXXX](https://issues.apache.org/jira/browse/WW-XXXX)`
- **Issue tracker**: https://issues.apache.org/jira/projects/WW
- **Never submit a PR that fixes a suspected vulnerability.** Before opening a PR, verify the change is not a security patch (OGNL injection, parameter
filtering bypass, file upload exploit, auth bypass, RCE, SSRF, path traversal, deserialization, XSS in framework components, etc.). If it is, stop and report
it privately to `security@struts.apache.org` — see [`SECURITY.md`](SECURITY.md).
**For comprehensive documentation quality analysis, use:** `/quality_check`
### Documentation Requirements
- **Always document security implications** in methods handling files/user input
- **Include usage examples** for complex methods and classes
- **Document exception conditions** and error handling behavior
- **Reference related methods** using `@see` tags
- **Explain resource management** responsibilities
## Error Handling & Logging
### Logging Best Practices
- Use parameterized logging for performance: `LOG.debug("Processing: {}", value)`
- Log security-relevant operations appropriately
- Use appropriate log levels (debug/info/warn/error)
- Avoid logging sensitive information
## Code Quality Standards
**For comprehensive code quality analysis, use:** `/quality_check`
### Key Principles
- Use `protected` for methods that subclasses might override
- Catch specific exceptions rather than generic `Exception`
- Use clear, descriptive method and variable names
- Follow existing project conventions and patterns
## Available Automated Tools
### Commands
- `/security_scan` - Comprehensive security analysis
- `/run_tests` - Intelligent test execution and analysis
- `/quality_check` - Code quality and documentation analysis
- `/config_analyze` - Configuration validation and optimization
- `/create_plan` - Implementation planning assistance
- `/validate_plan` - Plan validation and verification
- `/commit` - Guided git commit creation
- `/research_codebase` - Comprehensive codebase research
### Specialized Agents
- `security-analyzer` - OGNL injection scanning, CVE detection
- `test-runner` - Maven test execution and coverage analysis
- `code-quality-checker` - JavaDoc compliance, pattern consistency
- `config-validator` - struts.xml validation, interceptor analysis
- `jakarta-migration-helper` - Jakarta EE migration assistance
- `codebase-analyzer` - Project structure and architecture analysis
- `codebase-locator` - Code and file location assistance
- `codebase-pattern-finder` - Pattern examples and usage
## Common Pitfalls to Avoid
1. **File Security**: Never use `File.createTempFile()` without directory control
2. **Resource Leaks**: Always track and clean up temporary files
3. **Test Coverage**: Don't forget to test error conditions and cleanup
4. **Documentation**: Always document security implications
5. **Exception Handling**: Don't let cleanup failures affect main operations
6. **Path Validation**: Always validate and sanitize file paths
Vendored
+3 -21
View File
@@ -1,22 +1,4 @@
#!groovy
/*
* 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.
*/
pipeline {
agent none
@@ -105,7 +87,7 @@ pipeline {
when {
anyOf {
branch 'main'
branch 'support/struts-6-x-x'
branch 'release/struts-6-7-x'
}
}
steps {
@@ -119,7 +101,7 @@ pipeline {
when {
anyOf {
branch 'main'
branch 'support/struts-6-x-x'
branch 'release/struts-6-7-x'
}
}
steps {
@@ -132,7 +114,7 @@ pipeline {
when {
anyOf {
branch 'main'
branch 'support/struts-6-x-x'
branch 'release/struts-6-7-x'
}
}
steps {
+7 -93
View File
@@ -5,17 +5,13 @@
Please visit the [Releases](https://struts.apache.org/releases.html#prior-releases) page to see full information about each version
and what potential vulnerability it can have:
| Version | Supported |
|---------|-----------|
| 7.x | yes |
| 6.x.x | yes |
| 2.5.x | no |
| 2.3.x | no |
| 2.2.x | no |
| 2.1.x | no |
| 2.0.x | no |
| Version | Supported |
|---------|--------------------|
| 7.x | :white_check_mark: |
| 6.7.x | :white_check_mark: |
| 2.5.x | |
## Reporting New Security Issues with the Apache Struts
## Reporting New Security Issues with thr Apache Struts
([original](https://struts.apache.org/security.html))
@@ -33,7 +29,7 @@ All mail sent to this address that does not relate to security problems in the A
```
Note that all networked servers are subject to denial of service attacks, and we cannot promise magic
workarounds to generic problems (such as a client streaming lots of data to your server or requesting
workarounds to generic problems (such as a client streaming lots of data to your server, or requesting
the same URL repeatedly). In general, our philosophy is to avoid any attacks that can cause the server
to consume resources in a non-linear relationship to the size of inputs.
@@ -42,85 +38,3 @@ The mailing address is: [security@struts.apache.org](mailto:security@struts.apac
[General network server security tips](http://httpd.apache.org/docs/trunk/misc/security_tips.html)
[The Apache Security Team](http://www.apache.org/security/)
## Before Reporting
Before sending a vulnerability report, run through the following checks. They exist to prevent duplicate reports, public disclosure of untriaged issues,
and reports for behavior that is already documented as insecure configuration.
### 1. Read this policy
Confirm:
- which Struts versions are currently supported (see [Supported Versions](#supported-versions)),
- where reports must be sent (see [Reporting New Security Issues](#reporting-new-security-issues-with-the-apache-struts)),
- which reports do not belong on the private security list.
### 2. Read the Struts security guidelines
Review the [Struts security guidance](https://struts.apache.org/security/) and determine whether the finding is already covered by documented secure
configuration or application guidance, including but not limited to:
- Config Browser Plugin exposure,
- direct JSP access,
- `devMode` is required to exploit the vulnerability,
- `@StrutsParameter` usage and parameter annotation requirements,
- unsafe setters or getters exposed to request parameters,
- use of incoming values in localization or forced OGNL evaluation,
- raw JSP EL expressions,
- custom error pages,
- Dynamic Method Invocation and Strict Method Invocation,
- accepted and excluded parameter patterns,
- Fetch Metadata, COOP, and COEP protections,
- OGNL sandboxing, allowlists, excluded classes/packages, and OGNL Guard settings.
If the behavior is caused by an application ignoring documented security guidance, that is not an Apache Struts framework vulnerability.
### 3. Check previously disclosed vulnerabilities
Compare the finding against already disclosed Struts vulnerabilities — affected versions, impact ratings, mitigations, and fixed versions:
- [Struts security information](https://struts.apache.org/security/)
- [Prior releases and vulnerability notes](https://struts.apache.org/releases.html#prior-releases)
- [Security Bulletins (S2 series)](https://cwiki.apache.org/confluence/display/WW/Security+Bulletins)
If the finding overlaps with a known vulnerability, link to the existing bulletin, advisory, CVE, or release notes instead of drafting a new report.
## Assessment
Before drafting a report, confirm:
1. Is the affected version supported?
2. Is the behavior in Apache Struts framework code, rather than only in an application using Struts?
3. Is it already documented as insecure configuration or unsupported usage?
4. Is it a duplicate of a previously disclosed vulnerability or Security Bulletin?
5. Can the impact be demonstrated with a minimal, self-contained reproduction?
Only proceed with a private report when these answers still point to a likely new vulnerability in the framework.
## Private Report Requirements
A useful private report includes:
- affected Struts version or version range,
- affected component or module,
- required application configuration, if any,
- minimal reproduction steps,
- expected behavior,
- actual behavior,
- demonstrated security impact,
- whether authentication or special privileges are required,
- proposed fix or mitigation, if known.
Do not speculate beyond what can be demonstrated. If severity is uncertain, say so explicitly.
## Report Quality Rules
- One vulnerability per report.
- Keep reproduction steps minimal and self-contained.
- Do not include unrelated findings.
- Do not publish exploit details or proof-of-concept code publicly before the Struts project has triaged the issue. **Pushing a PoC to a public GitHub
repository, gist, fork, or branch counts as public disclosure** — even a "test" or throwaway repo. Private repositories are acceptable for sharing a PoC,
but access must be granted individually to each PMC member who will triage the report.
- Do not send ordinary bugs, usage questions, or generic denial-of-service concerns to the private security list.
- If the issue is not a vulnerability in Apache Struts source code, use the appropriate public support or issue channel instead.
+4 -1
View File
@@ -24,7 +24,7 @@
<parent>
<groupId>org.apache.struts</groupId>
<artifactId>struts2-parent</artifactId>
<version>7.2.0</version>
<version>7.1.1</version>
<relativePath>../parent/pom.xml</relativePath>
</parent>
<artifactId>struts2-apps</artifactId>
@@ -37,6 +37,9 @@
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.site.skip>true</maven.site.skip>
<maven.site.deploy.skip>true</maven.site.deploy.skip>
</properties>
<build>
-18
View File
@@ -1,18 +0,0 @@
# Rest Showcase
> **WARNING:** This application is a demonstration/development tool only. It is **NOT** intended for production
> deployment. Deploying this application on a publicly accessible server may pose security risks.
Rest Showcase is a simple example of a REST app built with the REST plugin.
For more on getting started with Struts, see:
- https://struts.apache.org/getting-started/
## I18N
Please note that this project was created with the assumption that it will be run in an environment where the default
locale is set to English. This means that the default messages defined in `package.properties` are in English.
If the default locale for your server is different, then rename `package.properties` to `package_en.properties` and
create a new `package.properties` with proper values for your default locale.
+15
View File
@@ -0,0 +1,15 @@
README.txt - Rest Showcase Webapp
Rest Showcase is a simple example of REST app build with the REST plugin.
For more on getting started with Struts, see
* http://cwiki.apache.org/WW/home.html
I18N:
=====
Please note that this project was created with the assumption that it will be run
in an environment where the default locale is set to English. This means that
the default messages defined in package.properties are in English. If the default
locale for your server is different, then rename package.properties to package_en.properties
and create a new package.properties with proper values for your default locale.
+2 -2
View File
@@ -24,12 +24,12 @@
<parent>
<groupId>org.apache.struts</groupId>
<artifactId>struts2-apps</artifactId>
<version>7.2.0</version>
<version>7.1.1</version>
</parent>
<artifactId>struts2-rest-showcase</artifactId>
<packaging>war</packaging>
<version>7.2.0</version>
<version>7.1.1</version>
<name>Struts 2 Rest Showcase Webapp</name>
<description>Struts 2 Rest Showcase Example</description>
-19
View File
@@ -1,19 +0,0 @@
# Showcase
> **WARNING:** This application is a demonstration/development tool only. It is **NOT** intended for production
> deployment. It contains features such as source code viewing that intentionally expose internal application details.
> Deploying this application on a publicly accessible server may pose security risks.
Showcase is a collection of examples with code that you might adopt and adapt in your own applications.
For more on getting started with Struts, see:
- https://struts.apache.org/getting-started/
## I18N
Please note that this project was created with the assumption that it will be run in an environment where the default
locale is set to English. This means that the default messages defined in `package.properties` are in English.
If the default locale for your server is different, then rename `package.properties` to `package_en.properties` and
create a new `package.properties` with proper values for your default locale.
+16
View File
@@ -0,0 +1,16 @@
README.txt - showcase
Showcase is a collection of examples with code that you might be adopt and
adapt in your own applications.
For more on getting started with Struts, see
* http://cwiki.apache.org/WW/home.html
I18N:
=====
Please note that this project was created with the assumption that it will be run
in an environment where the default locale is set to English. This means that
the default messages defined in package.properties are in English. If the default
locale for your server is different, then rename package.properties to package_en.properties
and create a new package.properties with proper values for your default locale.
+3 -7
View File
@@ -24,7 +24,7 @@
<parent>
<groupId>org.apache.struts</groupId>
<artifactId>struts2-apps</artifactId>
<version>7.2.0</version>
<version>7.1.1</version>
</parent>
<artifactId>struts2-showcase</artifactId>
@@ -119,10 +119,6 @@
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-slf4j-impl</artifactId>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-web</artifactId>
</dependency>
<dependency>
<groupId>org.sitemesh</groupId>
@@ -170,7 +166,7 @@
<dependency>
<groupId>org.codehaus.groovy</groupId>
<artifactId>groovy-jsr223</artifactId>
<version>3.0.25</version>
<version>3.0.22</version>
</dependency>
</dependencies>
@@ -211,7 +207,7 @@
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-failsafe-plugin</artifactId>
<version>3.5.6</version>
<version>3.5.4</version>
<configuration>
<includes>
<include>it.org.apache.struts2.showcase.*Test</include>
@@ -1,32 +0,0 @@
/*
* 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.struts2.showcase.action;
import org.apache.struts2.ActionSupport;
public class Html5Action extends ActionSupport {
@Override
public String execute() throws Exception {
addActionError("Action error: only html5");
addActionMessage("Action message: only html5");
addFieldError("testField", "Field error: only html5");
return super.execute();
}
}
@@ -1,225 +0,0 @@
/*
* 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.struts2.showcase.fileupload;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.apache.struts2.ActionSupport;
import org.apache.struts2.Preparable;
import org.apache.struts2.ServletActionContext;
import org.apache.struts2.action.UploadedFilesAware;
import org.apache.struts2.dispatcher.multipart.UploadedFile;
import org.apache.struts2.interceptor.parameter.StrutsParameter;
import java.util.List;
/**
* <p>
* Demonstrates dynamic file upload validation using WithLazyParams.
* This action shows how file upload validation rules can be determined
* at runtime based on action properties, session data, or other dynamic values.
* </p>
*
* <p>
* The validation parameters (allowedTypes, allowedExtensions, maximumSize)
* are set dynamically in the prepare() method and then referenced in struts.xml
* using ${...} expressions. This allows the same action to enforce different
* validation rules based on runtime conditions.
* </p>
*
* <p>
* This example demonstrates two use cases:
* </p>
* <ul>
* <li><strong>Document Upload:</strong> Accepts PDF and Word documents up to 5MB</li>
* <li><strong>Image Upload:</strong> Accepts JPEG and PNG images up to 2MB</li>
* </ul>
*
* @see org.apache.struts2.interceptor.WithLazyParams
* @see org.apache.struts2.interceptor.ActionFileUploadInterceptor
*/
public class DynamicFileUploadAction extends ActionSupport implements Preparable, UploadedFilesAware {
private static final Logger LOG = LogManager.getLogger(DynamicFileUploadAction.class);
private UploadedFile uploadedFile;
private String contentType;
private String fileName;
private String originalName;
private String inputName;
private String uploadType = "document";
private transient UploadConfig uploadConfig;
@Override
public String input() {
return INPUT;
}
public String upload() {
if (uploadedFile == null) {
addActionError("Please select a file to upload");
return INPUT;
}
return SUCCESS;
}
@Override
public void withUploadedFiles(List<UploadedFile> uploadedFiles) {
if (!uploadedFiles.isEmpty()) {
LOG.info("Uploaded file: {}", uploadedFiles.get(0));
this.uploadedFile = uploadedFiles.get(0);
this.fileName = uploadedFile.getName();
this.contentType = uploadedFile.getContentType();
this.originalName = uploadedFile.getOriginalName();
this.inputName = uploadedFile.getInputName();
}
}
// Getters and Setters
public String getContentType() {
return contentType;
}
public String getFileName() {
return fileName;
}
public String getOriginalName() {
return originalName;
}
public String getInputName() {
return inputName;
}
public Object getUploadedFile() {
return uploadedFile != null ? uploadedFile.getContent() : null;
}
public long getUploadSize() {
return uploadedFile != null ? uploadedFile.length() : 0;
}
public String getUploadType() {
return uploadType;
}
@StrutsParameter
public void setUploadType(String uploadType) {
this.uploadType = uploadType;
}
@Override
public void prepare() throws Exception {
// no-op
}
public void prepareUpload() {
String type = ServletActionContext.getRequest().getParameter("uploadType");
prepareUploadConfig(type != null ? type : "document");
}
private void prepareUploadConfig(String uploadType) {
uploadConfig = new UploadConfig();
LOG.debug("Configure validation rules based on upload type: {}", uploadType);
if ("image".equals(uploadType)) {
// Image upload configuration
uploadConfig.setAllowedMimeTypes("image/jpeg,image/png");
uploadConfig.setAllowedExtensions(".jpg,.jpeg,.png");
uploadConfig.setMaxFileSize(2097152L); // 2MB
uploadConfig.setDescription("images (JPEG, PNG)");
} else {
// Document upload configuration (default)
uploadConfig.setAllowedMimeTypes("application/pdf,application/msword,application/vnd.openxmlformats-officedocument.wordprocessingml.document");
uploadConfig.setAllowedExtensions(".pdf,.doc,.docx");
uploadConfig.setMaxFileSize(5242880L); // 5MB
uploadConfig.setDescription("documents (PDF, Word)");
}
}
/**
* Returns the upload configuration object.
* This is used in struts.xml with ${uploadConfig.allowedMimeTypes} expressions.
*/
public UploadConfig getUploadConfig() {
return uploadConfig;
}
/**
* Configuration holder for dynamic file upload validation rules.
*/
public static class UploadConfig {
private String allowedMimeTypes;
private String allowedExtensions;
private Long maxFileSize;
private String description;
public String getAllowedMimeTypes() {
return allowedMimeTypes;
}
public void setAllowedMimeTypes(String allowedMimeTypes) {
this.allowedMimeTypes = allowedMimeTypes;
}
public String getAllowedExtensions() {
return allowedExtensions;
}
public void setAllowedExtensions(String allowedExtensions) {
this.allowedExtensions = allowedExtensions;
}
public Long getMaxFileSize() {
return maxFileSize;
}
public void setMaxFileSize(Long maxFileSize) {
this.maxFileSize = maxFileSize;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
/**
* Returns a human-readable string representation of the max file size.
*/
public String getMaxFileSizeFormatted() {
if (maxFileSize == null) {
return "unlimited";
}
if (maxFileSize < 1024) {
return maxFileSize + " bytes";
} else if (maxFileSize < 1024 * 1024) {
return (maxFileSize / 1024) + " KB";
} else {
return (maxFileSize / (1024 * 1024)) + " MB";
}
}
}
}
@@ -1,42 +0,0 @@
/*
* 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.struts2.showcase.proxy;
import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
/**
* Simple AOP interceptor that wraps actions in a Spring proxy.
* Used to test that Struts correctly handles Spring AOP proxied actions
* in action chaining scenarios (WW-5514).
*/
public class LoggingInterceptor implements MethodInterceptor {
private static final Logger LOG = LogManager.getLogger(LoggingInterceptor.class);
@Override
public Object invoke(MethodInvocation invocation) throws Throwable {
LOG.debug("Invoking method: {} on target: {}",
invocation.getMethod().getName(),
invocation.getThis().getClass().getName());
return invocation.proceed();
}
}
@@ -30,9 +30,7 @@ import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.URI;
import java.nio.file.Files;
import java.nio.file.Path;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;
@@ -92,11 +90,7 @@ public class ViewSourceAction extends ActionSupport implements ServletContextAwa
if (config != null && config.startsWith("file:/")) {
int pos = config.lastIndexOf(':');
configLine = Integer.parseInt(config.substring(pos + 1));
String fileUrl = config.substring(0, pos);
Path configPath = resolveAllowedConfigPath(fileUrl);
if (configPath != null) {
configLines = read(Files.newInputStream(configPath), configLine);
}
configLines = read(new URL(config.substring(0, pos)).openStream(), configLine);
}
return SUCCESS;
}
@@ -233,29 +227,6 @@ public class ViewSourceAction extends ActionSupport implements ServletContextAwa
return snippet;
}
/**
* Resolves the given file URL to a real path if it points to an XML file within the webapp's
* deployment directory, preventing arbitrary file reads via crafted config parameters.
*
* @return the resolved path if allowed, or null if the path is outside the webapp or not an XML file
*/
private Path resolveAllowedConfigPath(String fileUrl) {
try {
Path filePath = Path.of(new URI(fileUrl)).toRealPath();
String realBasePath = servletContext.getRealPath("/");
if (realBasePath == null) {
return null;
}
Path basePath = Path.of(realBasePath).toRealPath();
if (filePath.startsWith(basePath) && filePath.toString().endsWith(".xml")) {
return filePath;
}
return null;
} catch (Exception e) {
return null;
}
}
@Override
public void withServletContext(ServletContext arg0) {
this.servletContext = arg0;
@@ -30,8 +30,5 @@
<AppenderRef ref="STDOUT"/>
</Root>
<Logger name="org.apache.struts2" level="info"/>
<Logger name="org.apache.struts2.showcase.fileupload" level="debug"/>
<Logger name="org.apache.struts2.inject" level="debug"/>
<Logger name="org.apache.struts2.interceptor.ActionFileUploadInterceptor" level="debug"/>
</Loggers>
</Configuration>
@@ -20,26 +20,21 @@
*/
-->
<!DOCTYPE struts PUBLIC
"-//Apache Software Foundation//DTD Struts Configuration 6.0//EN"
"https://struts.apache.org/dtds/struts-6.0.dtd">
"-//Apache Software Foundation//DTD Struts Configuration 6.0//EN"
"https://struts.apache.org/dtds/struts-6.0.dtd">
<struts>
<package name="actionchaining" extends="struts-default" namespace="/actionchaining">
<action name="actionChain1" class="org.apache.struts2.showcase.actionchaining.ActionChain1">
<result type="chain">actionChain2</result>
</action>
<action name="actionChain2" class="org.apache.struts2.showcase.actionchaining.ActionChain2">
<result type="chain">actionChain3</result>
</action>
<action name="actionChain3" class="org.apache.struts2.showcase.actionchaining.ActionChain3">
<result>/WEB-INF/actionchaining/actionChainingResult.jsp</result>
</action>
<!-- Spring AOP Proxied Action Chain Test (WW-5514) -->
<action name="proxiedActionChain1" class="proxiedActionChain1">
<result type="chain">actionChain2</result>
</action>
</package>
<package name="actionchaining" extends="struts-default" namespace="/actionchaining">
<action name="actionChain1" class="org.apache.struts2.showcase.actionchaining.ActionChain1">
<result type="chain">actionChain2</result>
</action>
<action name="actionChain2" class="org.apache.struts2.showcase.actionchaining.ActionChain2">
<result type="chain">actionChain3</result>
</action>
<action name="actionChain3" class="org.apache.struts2.showcase.actionchaining.ActionChain3">
<result>/WEB-INF/actionchaining/actionChainingResult.jsp</result>
</action>
</package>
</struts>
@@ -20,66 +20,43 @@
*/
-->
<!DOCTYPE struts PUBLIC
"-//Apache Software Foundation//DTD Struts Configuration 6.0//EN"
"https://struts.apache.org/dtds/struts-6.0.dtd">
"-//Apache Software Foundation//DTD Struts Configuration 6.0//EN"
"https://struts.apache.org/dtds/struts-6.0.dtd">
<struts>
<constant name="struts.multipart.maxSize" value="10240"/>
<constant name="struts.multipart.maxSize" value="10240" />
<package name="fileupload" extends="struts-default" namespace="/fileupload">
<package name="fileupload" extends="struts-default" namespace="/fileupload">
<action name="upload" class="org.apache.struts2.showcase.fileupload.FileUploadAction" method="input">
<result>/WEB-INF/fileupload/upload.jsp</result>
</action>
<result>/WEB-INF/fileupload/upload.jsp</result>
</action>
<action name="doUpload" class="org.apache.struts2.showcase.fileupload.FileUploadAction" method="upload">
<result name="input">/WEB-INF/fileupload/upload.jsp</result>
<result>/WEB-INF/fileupload/upload-success.jsp</result>
</action>
<result name="input">/WEB-INF/fileupload/upload.jsp</result>
<result>/WEB-INF/fileupload/upload-success.jsp</result>
</action>
<action name="multipleUploadUsingList">
<result>/WEB-INF/fileupload/multipleUploadUsingList.jsp</result>
</action>
<action name="multipleUploadUsingList">
<result>/WEB-INF/fileupload/multipleUploadUsingList.jsp</result>
</action>
<action name="doMultipleUploadUsingList"
class="org.apache.struts2.showcase.fileupload.MultipleFileUploadUsingListAction" method="upload">
<result name="input">/WEB-INF/fileupload/multipleUploadUsingList.jsp</result>
<result>/WEB-INF/fileupload/multiple-success.jsp</result>
</action>
<action name="doMultipleUploadUsingList" class="org.apache.struts2.showcase.fileupload.MultipleFileUploadUsingListAction" method="upload">
<result name="input">/WEB-INF/fileupload/multipleUploadUsingList.jsp</result>
<result>/WEB-INF/fileupload/multiple-success.jsp</result>
</action>
<action name="multipleUploadUsingArray">
<result>/WEB-INF/fileupload/multipleUploadUsingArray.jsp</result>
</action>
<action name="multipleUploadUsingArray">
<result>/WEB-INF/fileupload/multipleUploadUsingArray.jsp</result>
</action>
<action name="doMultipleUploadUsingArray"
class="org.apache.struts2.showcase.fileupload.MultipleFileUploadUsingArrayAction" method="upload">
<result name="input">/WEB-INF/fileupload/multipleUploadUsingArray.jsp</result>
<result>/WEB-INF/fileupload/multiple-success.jsp</result>
</action>
<action name="doMultipleUploadUsingArray" class="org.apache.struts2.showcase.fileupload.MultipleFileUploadUsingArrayAction" method="upload">
<result name="input">/WEB-INF/fileupload/multipleUploadUsingArray.jsp</result>
<result>/WEB-INF/fileupload/multiple-success.jsp</result>
</action>
<!-- Dynamic File Upload with WithLazyParams -->
<action name="dynamicUpload" class="org.apache.struts2.showcase.fileupload.DynamicFileUploadAction"
method="input">
<result name="input">/WEB-INF/fileupload/dynamic-upload.jsp</result>
</action>
<action name="doDynamicUpload" class="org.apache.struts2.showcase.fileupload.DynamicFileUploadAction"
method="upload">
<!--
WithLazyParams allows dynamic parameter evaluation.
The ${...} expressions are evaluated at runtime from the ValueStack,
allowing validation rules to be determined by action state.
-->
<interceptor-ref name="defaultStack">
<param name="actionFileUpload.allowedTypes">${uploadConfig.allowedMimeTypes}</param>
<param name="actionFileUpload.allowedExtensions">${uploadConfig.allowedExtensions}</param>
<param name="actionFileUpload.maximumSize">${uploadConfig.maxFileSize}</param>
</interceptor-ref>
<result name="input">/WEB-INF/fileupload/dynamic-upload.jsp</result>
<result>/WEB-INF/fileupload/dynamic-upload-success.jsp</result>
</action>
</package>
</struts>
+51 -57
View File
@@ -20,88 +20,90 @@
*/
-->
<!DOCTYPE struts PUBLIC
"-//Apache Software Foundation//DTD Struts Configuration 6.0//EN"
"https://struts.apache.org/dtds/struts-6.0.dtd">
"-//Apache Software Foundation//DTD Struts Configuration 6.0//EN"
"https://struts.apache.org/dtds/struts-6.0.dtd">
<!-- START SNIPPET: xworkSample -->
<struts>
<!-- Some or all of these can be flipped to true for debugging -->
<constant name="struts.i18n.reload" value="false"/>
<constant name="struts.enable.DynamicMethodInvocation" value="true"/>
<constant name="struts.devMode" value="false"/>
<constant name="struts.configuration.xml.reload" value="false"/>
<constant name="struts.custom.i18n.resources" value="globalMessages"/>
<constant name="struts.action.extension" value="action,,"/>
<constant name="struts.i18n.reload" value="false" />
<constant name="struts.enable.DynamicMethodInvocation" value="true" />
<constant name="struts.devMode" value="false" />
<constant name="struts.configuration.xml.reload" value="false" />
<constant name="struts.custom.i18n.resources" value="globalMessages" />
<constant name="struts.action.extension" value="action,," />
<constant name="struts.allowlist.enable" value="true"/>
<constant name="struts.parameters.requireAnnotations" value="true"/>
<constant name="struts.allowlist.packageNames" value="org.apache.struts2.showcase"/>
<constant name="struts.allowlist.enable" value="true" />
<constant name="struts.parameters.requireAnnotations" value="true" />
<constant name="struts.allowlist.packageNames" value="
org.apache.struts2.showcase.model,
org.apache.struts2.showcase.modelDriven.model
"/>
<constant name="struts.allowlist.classes" value="
org.apache.struts2.showcase.hangman.Hangman,
org.apache.struts2.showcase.hangman.Vocab
"/>
<!-- Enable Spring AOP proxy support for action chaining test (WW-5514) -->
<constant name="struts.disallowProxyObjectAccess" value="false"/>
<constant name="struts.convention.package.locators.basePackage" value="org.apache.struts2.showcase"/>
<constant name="struts.convention.result.path" value="/WEB-INF"/>
<constant name="struts.convention.package.locators.basePackage" value="org.apache.struts2.showcase" />
<constant name="struts.convention.result.path" value="/WEB-INF" />
<!-- Necessary for Showcase because default includes org.apache.struts2.* -->
<constant name="struts.convention.exclude.packages"
value="org.apache.struts.*,org.springframework.web.struts.*,org.springframework.web.struts2.*,org.hibernate.*"/>
<constant name="struts.convention.exclude.packages" value="org.apache.struts.*,org.springframework.web.struts.*,org.springframework.web.struts2.*,org.hibernate.*"/>
<constant name="struts.freemarker.manager.classname" value="customFreemarkerManager"/>
<constant name="struts.serve.static" value="true"/>
<constant name="struts.serve.static.browserCache" value="false"/>
<constant name="struts.freemarker.manager.classname" value="customFreemarkerManager" />
<constant name="struts.serve.static" value="true" />
<constant name="struts.serve.static.browserCache" value="false" />
<constant name="struts.action.excludePattern"
value=".*/images/.*\.gif,.*/img/.*\.gif,.*/styles/.*\.css,.*/js/.*\.js,/testServlet/.*"/>
<constant name="struts.action.excludePattern" value=".*/images/.*\.gif,.*/img/.*\.gif,.*/styles/.*\.css,.*/js/.*\.js,/testServlet/.*"/>
<include file="struts-interactive.xml"/>
<include file="struts-interactive.xml" />
<include file="struts-hangman.xml"/>
<include file="struts-hangman.xml" />
<include file="struts-tags.xml"/>
<include file="struts-validation.xml"/>
<include file="struts-validation.xml" />
<include file="struts-actionchaining.xml"/>
<include file="struts-actionchaining.xml" />
<include file="struts-fileupload.xml"/>
<include file="struts-fileupload.xml" />
<include file="struts-person.xml"/>
<include file="struts-person.xml" />
<include file="struts-wait.xml"/>
<include file="struts-wait.xml" />
<include file="struts-token.xml"/>
<include file="struts-token.xml" />
<include file="struts-model-driven.xml"/>
<include file="struts-model-driven.xml" />
<include file="struts-filedownload.xml"/>
<include file="struts-filedownload.xml" />
<include file="struts-conversion.xml"/>
<include file="struts-conversion.xml" />
<include file="struts-freemarker.xml"/>
<include file="struts-freemarker.xml" />
<include file="struts-tiles.xml"/>
<include file="struts-tiles.xml" />
<include file="struts-xslt.xml"/>
<include file="struts-xslt.xml" />
<include file="struts-async.xml"/>
<include file="struts-async.xml" />
<include file="struts-dispatcher.xml"/>
<include file="struts-dispatcher.xml" />
<include file="struts-params-annotation.xml"/>
<include file="struts-params-annotation.xml" />
<package name="default" extends="struts-default">
<interceptors>
<interceptor-stack name="crudStack">
<interceptor-ref name="checkbox"/>
<interceptor-ref name="params"/>
<interceptor-ref name="staticParams"/>
<interceptor-ref name="defaultStack"/>
<interceptor-ref name="checkbox" />
<interceptor-ref name="params" />
<interceptor-ref name="staticParams" />
<interceptor-ref name="defaultStack" />
</interceptor-stack>
</interceptors>
<default-action-ref name="showcase"/>
<default-action-ref name="showcase" />
<action name="showcase">
<result>/WEB-INF/showcase.jsp</result>
@@ -130,7 +132,7 @@
</action>
<action name="edit" class="org.apache.struts2.showcase.action.SkillAction">
<result>/WEB-INF/empmanager/editSkill.jsp</result>
<interceptor-ref name="params"/>
<interceptor-ref name="params" />
<interceptor-ref name="basicStack"/>
</action>
<action name="save" class="org.apache.struts2.showcase.action.SkillAction" method="save">
@@ -151,11 +153,9 @@
<interceptor-ref name="basicStack"/>
</action>
<action name="edit-*" class="org.apache.struts2.showcase.action.EmployeeAction">
<param name="empId">{1}</param>
<param name="empId">{1}</param>
<result>/WEB-INF/empmanager/editEmployee.jsp</result>
<interceptor-ref name="crudStack">
<param name="validation.excludeMethods">execute</param>
</interceptor-ref>
<interceptor-ref name="crudStack"><param name="validation.excludeMethods">execute</param></interceptor-ref>
</action>
<action name="save" class="org.apache.struts2.showcase.action.EmployeeAction" method="save">
<result name="input">/WEB-INF/empmanager/editEmployee.jsp</result>
@@ -167,13 +167,7 @@
</action>
</package>
<package name="html5" extends="default" namespace="/html5">
<action name="index" class="org.apache.struts2.showcase.action.Html5Action">
<result>/WEB-INF/html5/index.jsp</result>
</action>
</package>
</struts>
<!-- END SNIPPET: xworkSample -->
<!-- END SNIPPET: xworkSample -->
@@ -115,25 +115,5 @@
<bean id="guessCharacterAction" class="org.apache.struts2.showcase.hangman.GuessCharacterAction" scope="prototype"/>
<bean id="getUpdatedHangmanAction" class="org.apache.struts2.showcase.hangman.GetUpdatedHangmanAction"
scope="prototype"/>
<!-- Spring AOP Proxy Configuration for Action Chaining Test (WW-5514) -->
<bean id="loggingInterceptor" class="org.apache.struts2.showcase.proxy.LoggingInterceptor"/>
<bean class="org.springframework.aop.framework.autoproxy.BeanNameAutoProxyCreator">
<property name="proxyTargetClass" value="true"/>
<property name="beanNames">
<list>
<value>proxiedActionChain1</value>
</list>
</property>
<property name="interceptorNames">
<list>
<value>loggingInterceptor</value>
</list>
</property>
</bean>
<bean id="proxiedActionChain1" class="org.apache.struts2.showcase.actionchaining.ActionChain1" scope="prototype"/>
</beans>
@@ -132,49 +132,54 @@
<li class="dropdown">
<a href="#" class="dropdown-toggle" data-toggle="dropdown">Non UI Tags<b class="caret"></b></a>
<ul class="dropdown-menu" role="menu">
<li><s:url var="url" action="showActionTagDemo" namespace="/tags/non-ui/actionTag"/>
<s:a href="%{url}">Action Tag</s:a></li>
<li><s:url var="url" namespace="/tags/non-ui" action="date"/>
<s:a href="%{url}">Date Tag</s:a></li>
<li><s:url var="url" action="debugTagDemo" namespace="/tags/non-ui"/>
<s:a href="%{url}">Debug Tag</s:a></li>
<li><s:url var="url" action="showGeneratorTagDemo" namespace="/tags/non-ui/iteratorGeneratorTag"/>
<s:a href="%{url}">Iterator Generator Tag</s:a></li>
<li><s:url var="url" action="showActionTagDemo" namespace="/tags/non-ui/actionTag"/><s:a
href="%{url}">Action Tag</s:a></li>
<li><s:url var="url" namespace="/tags/non-ui" action="date"/><s:a
href="%{url}">Date Tag</s:a></li>
<li><s:url var="url" action="debugTagDemo" namespace="/tags/non-ui"/><s:a
href="%{url}">Debug Tag</s:a></li>
<li><s:url var="url" action="showGeneratorTagDemo"
namespace="/tags/non-ui/iteratorGeneratorTag"/><s:a
href="%{url}">Iterator Generator Tag</s:a></li>
<li>
<s:url var="url" action="showAppendTagDemo" namespace="/tags/non-ui/appendIteratorTag"/>
<s:url var="url" action="showAppendTagDemo"
namespace="/tags/non-ui/appendIteratorTag"/>
<s:a href="%{#url}">Append Iterator Tag</s:a>
<li>
<s:url var="url" action="showMergeTagDemo" namespace="/tags/non-ui/mergeIteratorTag"/>
<s:url var="url" action="showMergeTagDemo"
namespace="/tags/non-ui/mergeIteratorTag"/>
<s:a href="%{#url}">Merge Iterator Demo</s:a>
<li>
<s:url var="url" action="showSubsetTagDemo" namespace="/tags/non-ui/subsetIteratorTag"/>
<s:url var="url" action="showSubsetTagDemo"
namespace="/tags/non-ui/subsetIteratorTag"/>
<s:a href="%{#url}">Subset Tag</s:a>
<li><s:url var="url" action="actionPrefixExampleUsingFreemarker" namespace="/tags/non-ui/actionPrefix"/>
<s:a href="%{#url}">Action Prefix Example (Freemarker)</s:a></li>
<li><s:url var="url" action="testIfTagJsp" namespace="/tags/non-ui/ifTag"/>
<s:a href="%{#url}">If Tag (JSP)</s:a></li>
<li><s:url var="url" action="testIfTagFreemarker" namespace="/tags/non-ui/ifTag"/>
<s:a href="%{#url}">If Tag (Freemarker)</s:a></li>
<li><s:url var="url" action="actionPrefixExampleUsingFreemarker"
namespace="/tags/non-ui/actionPrefix"/><s:a
href="%{#url}">Action Prefix Example (Freemarker)</s:a></li>
<li><s:url var="url" action="testIfTagJsp" namespace="/tags/non-ui/ifTag"/><s:a
href="%{#url}">If Tag (JSP)</s:a></li>
<li><s:url var="url" action="testIfTagFreemarker"
namespace="/tags/non-ui/ifTag"/><s:a
href="%{#url}">If Tag (Freemarker)</s:a></li>
</ul>
</li>
<li class="dropdown">
<a href="#" class="dropdown-toggle" data-toggle="dropdown">UI Tags<b class="caret"></b></a>
<ul class="dropdown-menu" role="menu">
<li><s:url var="url" namespace="/tags/ui" action="example" method="input"/>
<s:a href="%{url}">UI Example</s:a></li>
<li><s:url var="url" namespace="/tags/ui" action="exampleVelocity" method="input"/>
<s:a href="%{url}">UI Example (Velocity)</s:a></li>
<li><s:url var="url" namespace="/tags/ui" action="lotsOfOptiontransferselect" method="input"/>
<s:a href="%{url}">Option Transfer Select UI Example</s:a></li>
<li><s:url var="url" namespace="/tags/ui" action="moreSelects" method="input"/>
<s:a href="%{url}">More Select Box UI Examples</s:a></li>
<li><s:url var="url" namespace="/tags/ui" action="example" method="input"/><s:a
href="%{url}">UI Example</s:a></li>
<li><s:url var="url" namespace="/tags/ui" action="exampleVelocity"
method="input"/><s:a href="%{url}">UI Example (Velocity)</s:a></li>
<li><s:url var="url" namespace="/tags/ui" action="lotsOfOptiontransferselect"
method="input"/><s:a
href="%{url}">Option Transfer Select UI Example</s:a></li>
<li><s:url var="url" namespace="/tags/ui" action="moreSelects" method="input"/><s:a
href="%{url}">More Select Box UI Examples</s:a></li>
<li>
<s:url var="url" namespace="/tags/ui" action="componentTagExample"/>
<s:a href="%{#url}">Component Tag Example</s:a></li>
<li><s:url var="url" namespace="/tags/ui" action="actionTagExample" method="input"/>
<s:a href="%{url}">Action Tag Example</s:a></li>
<li><s:url var="url" action="index" namespace="/html5"/>
<s:a href="%{#url}">Html 5 theme</s:a></li>
<li><s:url var="url" namespace="/tags/ui" action="actionTagExample" method="input"/><s:a
href="%{url}">Action Tag Example</s:a></li>
</ul>
</li>
<li class="dropdown">
@@ -187,10 +192,6 @@
<s:url var="url" action="upload" namespace="/fileupload"/>
<s:a href="%{#url}">Single File Upload</s:a>
</li>
<li>
<s:url var="url" action="dynamicUpload" namespace="/fileupload"/>
<s:a href="%{#url}">Single File Upload - dynamic config</s:a>
</li>
<li>
<s:url var="url" action="multipleUploadUsingList" namespace="/fileupload"/>
<s:a href="%{#url}">Multiple File Upload (List)</s:a>
@@ -266,7 +267,7 @@
<ul class="nav navbar-nav pull-right">
<li class="dropdown last">
<a href="#" class="dropdown-toggle" data-toggle="dropdown">
<i class="glyphicon glyphicon-question-sign"></i> Help<b class="caret"></b></a>
<i class="glyphicon glyphicon-question-sign"></i> Help<b lass="caret"></b></a>
<ul class="dropdown-menu">
<s:url var="help" action="help" namespace="/" includeContext="false" />
<li><s:a value="%{help}">Help</s:a></li>
@@ -312,7 +313,9 @@
<div class="pull-left">
Copyright &copy; 2003-<s:property value="#dateAction.now.year + 1900"/>
<a href="https://www.apache.org">The Apache Software Foundation.</a>
<a href="http://www.apache.org">
The Apache Software Foundation.
</a>
</div>
</footer>
</body>
@@ -1,109 +0,0 @@
<!DOCTYPE html>
<!--
/*
* 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.
*/
-->
<%@ page
language="java"
contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8" %>
<%@ taglib prefix="s" uri="/struts-tags" %>
<html lang="en">
<head>
<title>Struts2 Showcase - Dynamic File Upload Success</title>
</head>
<body>
<div class="page-header">
<h1>File Upload Successful</h1>
<p class="lead">Your file was validated and uploaded successfully</p>
</div>
<div class="container-fluid">
<div class="row">
<div class="col-md-12">
<div class="alert alert-success">
<strong>Success!</strong> Your file passed all validation checks.
</div>
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title">Upload Details</h3>
</div>
<div class="panel-body">
<dl class="dl-horizontal">
<dt>Upload Type:</dt>
<dd><s:property value="uploadType == 'image' ? 'Image' : 'Document'"/></dd>
<dt>Content Type:</dt>
<dd><code><s:property value="contentType"/></code></dd>
<dt>File Name:</dt>
<dd><s:property value="fileName"/></dd>
<dt>Original Name:</dt>
<dd><s:property value="originalName"/></dd>
<dt>File Size:</dt>
<dd><s:property value="uploadSize"/> bytes</dd>
<dt>Input Name:</dt>
<dd><s:property value="inputName"/></dd>
<dt>File Object:</dt>
<dd><code><s:property value="uploadedFile"/></code></dd>
</dl>
</div>
</div>
<div class="panel panel-info">
<div class="panel-heading">
<h3 class="panel-title">Validation Rules Applied</h3>
</div>
<div class="panel-body">
<dl class="dl-horizontal">
<dt>Allowed MIME Types:</dt>
<dd><code><s:property value="uploadConfig.allowedMimeTypes"/></code></dd>
<dt>Allowed Extensions:</dt>
<dd><code><s:property value="uploadConfig.allowedExtensions"/></code></dd>
<dt>Maximum Size:</dt>
<dd><s:property value="uploadConfig.maxFileSizeFormatted"/></dd>
</dl>
<p class="text-muted">
<small>
These validation rules were determined dynamically at runtime
using <code>WithLazyParams</code> and evaluated from the ValueStack.
</small>
</p>
</div>
</div>
<div class="btn-group">
<s:a action="dynamicUpload" cssClass="btn btn-primary">
<i class="glyphicon glyphicon-upload"></i> Upload Another File
</s:a>
</div>
</div>
</div>
</div>
</body>
</html>
@@ -1,101 +0,0 @@
<!DOCTYPE html>
<!--
/*
* 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.
*/
-->
<%@ taglib prefix="s" uri="/struts-tags" %>
<html lang="en">
<head>
<title>Struts2 Showcase - Dynamic File Upload Validation</title>
</head>
<body>
<div class="page-header">
<h1>Dynamic File Upload Validation</h1>
<p class="lead">Demonstrates WithLazyParams for runtime validation rules</p>
</div>
<div class="container-fluid">
<div class="row">
<div class="col-md-12">
<div class="alert alert-info">
<h4>About This Example</h4>
<p>
This example demonstrates how to use <code>WithLazyParams</code> to configure
file upload validation rules dynamically at runtime. The validation parameters
(<code>allowedTypes</code>, <code>allowedExtensions</code>, <code>maximumSize</code>)
are evaluated from the ValueStack for each request, allowing different rules
based on action state, user permissions, or other runtime conditions.
</p>
</div>
<div class="alert alert-success">
<h4>Current Configuration</h4>
<ul>
<li><strong>Upload Type:</strong> <s:property
value="uploadType == 'image' ? 'Image Upload' : 'Document Upload'"/></li>
<li><strong>Allowed Types:</strong> <code><s:property value="uploadConfig.allowedMimeTypes"/></code>
</li>
<li><strong>Allowed Extensions:</strong> <code><s:property
value="uploadConfig.allowedExtensions"/></code></li>
<li><strong>Maximum Size:</strong> <s:property value="uploadConfig.maxFileSizeFormatted"/></li>
<li><strong>Description:</strong> <s:property value="uploadConfig.description"/></li>
</ul>
</div>
</div>
</div>
<s:actionerror cssClass="alert alert-danger"/>
<s:fielderror cssClass="alert alert-warning"/>
<div class="row">
<div class="col-md-12">
<s:form action="doDynamicUpload" method="POST" enctype="multipart/form-data" cssClass="form-vertical">
<s:radio name="uploadType" label="Upload type"
list="#{'document':'Documents (PDF, Word) - up to 5MB', 'image':'Images (JPEG, PNG) - up to 2MB'}"/>
<s:file name="upload" label="Select File" cssClass="form-control"/>
<s:submit value="Upload File" cssClass="btn btn-primary"/>
<s:submit value="Refresh Rules" action="dynamicUpload" cssClass="btn btn-default"/>
</s:form>
</div>
</div>
<div class="row">
<div class="col-md-12">
<div class="well">
<h4>How It Works</h4>
<p>In <code>struts.xml</code>, the interceptor parameters use expressions:</p>
<pre>&lt;interceptor-ref name="actionFileUpload"&gt;
&lt;param name="allowedTypes"&gt;<strong>${uploadConfig.allowedMimeTypes}</strong>&lt;/param&gt;
&lt;param name="allowedExtensions"&gt;<strong>${uploadConfig.allowedExtensions}</strong>&lt;/param&gt;
&lt;param name="maximumSize"&gt;<strong>${uploadConfig.maxFileSize}</strong>&lt;/param&gt;
&lt;/interceptor-ref&gt;</pre>
<p>
These expressions are evaluated at runtime against the ValueStack,
allowing the action to control validation rules dynamically in its
<code>prepare()</code> method.
</p>
</div>
</div>
</div>
</div>
</body>
</html>
@@ -1,283 +0,0 @@
<!--
/*
* 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.
*/
-->
<%@ taglib prefix="s" uri="/struts-tags" %>
<!DOCTYPE html>
<s:compress>
<html lang="en">
<head>
<s:url var="bootstrapCss" value="/styles/bootstrap.css" encode="false" includeParams="none"/>
<s:link theme="html5" href="%{bootstrapCss}"/>
<s:url var="mainCss" value="/styles/main.css" encode="false" includeParams="none"/>
<s:link theme="html5" href="%{mainCss}" />
<s:head theme="html5"/>
<title>Struts2 Showcase - Html5 theme</title>
</head>
<body>
<div class="container-fluid">
<div class="row">
<div class="col-md-12">
<div class="hero-unit">
<h1>Html 5 tags demo</h1>
<p>All the tags on this page are from <i>html5</i> theme. <s:a theme="html5" action="showcase" namespace="/">Back</s:a> to main Showcase App page</p>
</div>
</div>
</div>
<!-- Section 1: Link Components -->
<div class="row">
<div class="col-md-12">
<div class="page-header">
<h2>Link Components</h2>
</div>
</div>
</div>
<div class="row">
<div class="col-md-2">
<pre>&lt;s:a/&gt;</pre>
</div>
<div class="col-md-10">
<s:a theme="html5" action="index">index</s:a>
</div>
</div>
<!-- Section 2: Error & Message Components -->
<div class="row">
<div class="col-md-12">
<div class="page-header">
<h2>Error &amp; Message Components</h2>
</div>
</div>
</div>
<div class="row">
<div class="col-md-2">
<pre>&lt;s:actionerror/&gt;</pre>
</div>
<div class="col-md-10">
<s:actionerror theme="html5"/>
</div>
</div>
<div class="row">
<div class="col-md-2">
<pre>&lt;s:actionmessage/&gt;</pre>
</div>
<div class="col-md-10">
<s:actionmessage theme="html5"/>
</div>
</div>
<div class="row">
<div class="col-md-2">
<pre>&lt;s:fielderror/&gt;</pre>
</div>
<div class="col-md-10">
<s:fielderror theme="html5"/>
</div>
</div>
<!-- Section 3: Form Components -->
<div class="row">
<div class="col-md-12">
<div class="page-header">
<h2>Form Components</h2>
</div>
</div>
</div>
<s:form theme="html5" action="index" method="post">
<div class="row">
<div class="col-md-2">
<pre>&lt;s:form/&gt;</pre>
</div>
<div class="col-md-10">
<p>Form wrapper (wraps all components below)</p>
</div>
</div>
<div class="row">
<div class="col-md-2">
<pre>&lt;s:textfield/&gt;</pre>
</div>
<div class="col-md-10">
<s:textfield theme="html5" label="Name" name="name" tooltip="Enter your name here"/>
</div>
</div>
<div class="row">
<div class="col-md-2">
<pre>&lt;s:password/&gt;</pre>
</div>
<div class="col-md-10">
<s:password theme="html5" label="Password" name="password"/>
</div>
</div>
<div class="row">
<div class="col-md-2">
<pre>&lt;s:textarea/&gt;</pre>
</div>
<div class="col-md-10">
<s:textarea theme="html5" label="Comments" name="comments" cols="40" rows="3"/>
</div>
</div>
<div class="row">
<div class="col-md-2">
<pre>&lt;s:hidden/&gt;</pre>
</div>
<div class="col-md-10">
<s:hidden theme="html5" name="hiddenValue" value="secret"/>
<p><small>Hidden field with value="secret" (not visible)</small></p>
</div>
</div>
<div class="row">
<div class="col-md-2">
<pre>&lt;s:checkbox/&gt;</pre>
</div>
<div class="col-md-10">
<s:checkbox theme="html5" label="Accept Terms" name="terms"/>
</div>
</div>
<div class="row">
<div class="col-md-2">
<pre>&lt;s:radio/&gt;</pre>
</div>
<div class="col-md-10">
<s:radio theme="html5" label="Gender" list="{'Male', 'Female', 'Other'}" name="gender"/>
</div>
</div>
<div class="row">
<div class="col-md-2">
<pre>&lt;s:select/&gt;</pre>
</div>
<div class="col-md-10">
<s:select theme="html5" label="Country" list="{'USA', 'UK', 'Canada', 'Australia'}" name="country" emptyOption="true" headerKey="" headerValue="-- Please Select --"/>
</div>
</div>
<div class="row">
<div class="col-md-2">
<pre>&lt;s:checkboxlist/&gt;</pre>
</div>
<div class="col-md-10">
<s:checkboxlist theme="html5" label="Interests" list="{'Sports', 'Music', 'Reading', 'Travel'}" name="interests"/>
</div>
</div>
<div class="row">
<div class="col-md-2">
<pre>&lt;s:file/&gt;</pre>
</div>
<div class="col-md-10">
<s:file theme="html5" label="Upload File" name="upload"/>
</div>
</div>
<div class="row">
<div class="col-md-2">
<pre>&lt;s:token/&gt;</pre>
</div>
<div class="col-md-10">
<s:token theme="html5"/>
<p><small>CSRF token (hidden, check page source)</small></p>
</div>
</div>
<div class="row">
<div class="col-md-2">
<pre>&lt;s:submit/&gt;</pre>
</div>
<div class="col-md-10">
<s:submit theme="html5" value="Submit" cssClass="btn btn-primary"/>
</div>
</div>
<div class="row">
<div class="col-md-2">
<pre>&lt;s:reset/&gt;</pre>
</div>
<div class="col-md-10">
<s:reset theme="html5" value="Reset" cssClass="btn btn-danger"/>
</div>
</div>
</s:form>
<!-- Section 4: Advanced Selection Components -->
<div class="row">
<div class="col-md-12">
<div class="page-header">
<h2>Advanced Selection Components</h2>
</div>
</div>
</div>
<div class="row">
<div class="col-md-2">
<pre>&lt;s:combobox/&gt;</pre>
</div>
<div class="col-md-10">
<s:combobox theme="html5" label="Favourite City" name="city" list="{'New York', 'London', 'Tokyo', 'Paris'}"/>
</div>
</div>
<!-- Section 5: Utility & Display Components -->
<div class="row">
<div class="col-md-12">
<div class="page-header">
<h2>Utility &amp; Display Components</h2>
</div>
</div>
</div>
<div class="row">
<div class="col-md-2">
<pre>&lt;s:label/&gt;</pre>
</div>
<div class="col-md-10">
<s:label theme="html5" label="Display Label" name="displayValue" value="Read-only Value"/>
</div>
</div>
<div class="row">
<div class="col-md-2">
<pre>&lt;s:script/&gt;</pre>
</div>
<div class="col-md-10">
<s:script theme="html5">
console.log('HTML5 theme script tag example');
</s:script>
<p><small>Script tag (check browser console for output)</small></p>
</div>
</div>
<div class="row">
<div class="col-md-2">
<pre>&lt;s:debug/&gt;</pre>
</div>
<div class="col-md-10">
<s:debug theme="html5"/>
</div>
</div>
<div class="row">
<div class="col-md-2">
<pre>&lt;s:head/&gt;</pre>
</div>
<div class="col-md-10">
<p><small>Already used in page &lt;head&gt; section (line 30)</small></p>
</div>
</div>
<div class="row">
<div class="col-md-2">
<pre>&lt;s:link/&gt;</pre>
</div>
<div class="col-md-10">
<p><small>Already used in page &lt;head&gt; section for CSS (lines 27, 29)</small></p>
</div>
</div>
</div>
</body>
</html>
</s:compress>
@@ -27,5 +27,4 @@
<mapping path="/images/*" exclude="true"/>
<mapping path="/static/*" exclude="true"/>
<mapping path="/nodecorate/*" exclude="true"/>
<mapping path="/html5/*" exclude="true"/>
</sitemesh>
@@ -1,282 +0,0 @@
/*
* 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 it.org.apache.struts2.showcase;
import org.assertj.core.api.Assertions;
import org.htmlunit.WebClient;
import org.htmlunit.html.HtmlFileInput;
import org.htmlunit.html.HtmlForm;
import org.htmlunit.html.HtmlPage;
import org.htmlunit.html.HtmlRadioButtonInput;
import org.htmlunit.html.HtmlSubmitInput;
import org.junit.Test;
import java.io.File;
import java.io.FileOutputStream;
import java.io.FileWriter;
import java.security.SecureRandom;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Integration tests for dynamic file upload validation feature.
* Tests the WithLazyParams functionality that allows runtime configuration
* of file upload validation rules based on action state.
*/
public class DynamicFileUploadTest {
@Test
public void testDynamicUploadValidDocument() throws Exception {
try (final WebClient webClient = new WebClient()) {
final HtmlPage page = webClient.getPage(ParameterUtils.getBaseUrl() + "/fileupload/dynamicUpload.action");
final HtmlForm form = page.getForms().get(0);
// Select document upload type
HtmlRadioButtonInput documentRadio = form.getInputByValue("document");
documentRadio.setChecked(true);
// Create a small PDF-like file
File pdfFile = createTestFile("test.pdf", 1024);
pdfFile.deleteOnExit();
HtmlFileInput uploadInput = form.getInputByName("upload");
uploadInput.setFiles(pdfFile);
final HtmlSubmitInput button = form.getInputByValue("Upload File");
final HtmlPage resultPage = button.click();
String content = resultPage.getVisibleText();
assertThat(content).contains(
"File Upload Successful",
"Upload Type:\nDocument",
"Original Name:\n" + pdfFile.getName()
);
}
}
@Test
public void testDynamicUploadValidImage() throws Exception {
try (final WebClient webClient = new WebClient()) {
final HtmlPage page = webClient.getPage(ParameterUtils.getBaseUrl() + "/fileupload/dynamicUpload.action");
final HtmlForm form = page.getForms().get(0);
// Select image upload type
HtmlRadioButtonInput imageRadio = form.getInputByValue("image");
imageRadio.setChecked(true);
assertThat(imageRadio)
.isNotNull()
.hasFieldOrProperty("value")
.isNotNull()
.extracting(HtmlRadioButtonInput::isChecked)
.asInstanceOf(Assertions.BOOLEAN).isTrue();
// Create a small image-like file
File imageFile = createTestFile("test.png", 1024);
imageFile.deleteOnExit();
HtmlFileInput uploadInput = form.getInputByName("upload");
uploadInput.setFiles(imageFile);
final HtmlSubmitInput button = form.getInputByValue("Upload File");
final HtmlPage resultPage = button.click();
String content = resultPage.getVisibleText();
assertThat(content).contains(
"File Upload Successful",
"Upload Type:\nImage",
"Original Name:\n" + imageFile.getName()
);
}
}
@Test
public void testDynamicUploadDocumentRejectsImage() throws Exception {
try (final WebClient webClient = new WebClient()) {
final HtmlPage page = webClient.getPage(ParameterUtils.getBaseUrl() + "/fileupload/dynamicUpload.action");
final HtmlForm form = page.getForms().get(0);
// Select document upload type
HtmlRadioButtonInput documentRadio = form.getInputByValue("document");
documentRadio.setChecked(true);
// Try to upload an image file
File imageFile = createTestFile("test.jpg", 512);
imageFile.deleteOnExit();
HtmlFileInput uploadInput = form.getInputByName("upload");
uploadInput.setFiles(imageFile);
final HtmlSubmitInput button = form.getInputByValue("Upload File");
final HtmlPage resultPage = button.click();
String content = resultPage.getVisibleText();
assertThat(content).contains(
"Content-Type not allowed",
"image/jpeg",
"File extension not allowed"
);
}
}
@Test
public void testDynamicUploadImageRejectsDocument() throws Exception {
try (final WebClient webClient = new WebClient()) {
final HtmlPage page = webClient.getPage(ParameterUtils.getBaseUrl() + "/fileupload/dynamicUpload.action");
final HtmlForm form = page.getForms().get(0);
// Select image upload type
HtmlRadioButtonInput imageRadio = form.getInputByValue("image");
imageRadio.setChecked(true);
// Try to upload a PDF file
File pdfFile = createTestFile("test.pdf", 512);
pdfFile.deleteOnExit();
HtmlFileInput uploadInput = form.getInputByName("upload");
uploadInput.setFiles(pdfFile);
final HtmlSubmitInput button = form.getInputByValue("Upload File");
final HtmlPage resultPage = button.click();
String content = resultPage.getVisibleText();
assertThat(content).contains(
"Content-Type not allowed",
"application/pdf",
"File extension not allowed"
);
}
}
@Test
public void testDynamicUploadDocumentExceedsMaxSize() throws Exception {
try (final WebClient webClient = new WebClient()) {
final HtmlPage page = webClient.getPage(ParameterUtils.getBaseUrl() + "/fileupload/dynamicUpload.action");
final HtmlForm form = page.getForms().get(0);
// Select document upload type (max 5MB)
HtmlRadioButtonInput documentRadio = form.getInputByValue("document");
documentRadio.setChecked(true);
// Create a file larger than 5MB
File largeFile = createLargeTestFile("large.pdf", 5 * 1024 * 1024 + 1024);
largeFile.deleteOnExit();
HtmlFileInput uploadInput = form.getInputByName("upload");
uploadInput.setFiles(largeFile);
final HtmlSubmitInput button = form.getInputByValue("Upload File");
final HtmlPage resultPage = button.click();
String content = resultPage.getVisibleText();
assertThat(content).contains("Request exceeded allowed size limit! Max size allowed is:");
}
}
@Test
public void testDynamicUploadImageExceedsMaxSize() throws Exception {
try (final WebClient webClient = new WebClient()) {
final HtmlPage page = webClient.getPage(ParameterUtils.getBaseUrl() + "/fileupload/dynamicUpload.action");
final HtmlForm form = page.getForms().get(0);
// Select image upload type (max 2MB)
HtmlRadioButtonInput imageRadio = form.getInputByValue("image");
imageRadio.setChecked(true);
// Create a file larger than 2MB
File largeFile = createLargeTestFile("large.png", 2 * 1024 * 1024 + 1024);
largeFile.deleteOnExit();
HtmlFileInput uploadInput = form.getInputByName("upload");
uploadInput.setFiles(largeFile);
final HtmlSubmitInput button = form.getInputByValue("Upload File");
final HtmlPage resultPage = button.click();
String content = resultPage.getVisibleText();
assertThat(content).containsAnyOf("size", "Size", "limit", "exceed");
}
}
@Test
public void testDynamicUploadSwitchBetweenModes() throws Exception {
try (final WebClient webClient = new WebClient()) {
final HtmlPage page = webClient.getPage(ParameterUtils.getBaseUrl() + "/fileupload/dynamicUpload.action");
// Verify initial state shows document mode by default
String initialContent = page.getVisibleText();
assertThat(initialContent).contains("Document Upload");
final HtmlForm form = page.getForms().get(0);
// Switch to image mode and refresh rules
HtmlRadioButtonInput imageRadio = form.getInputByValue("image");
imageRadio.setChecked(true);
final HtmlSubmitInput refreshButton = form.getInputByValue("Refresh Rules");
final HtmlPage refreshedPage = refreshButton.click();
// Verify rules changed to image mode
String refreshedContent = refreshedPage.getVisibleText();
assertThat(refreshedContent).contains(
"Image Upload",
"image/jpeg",
"2 MB"
);
}
}
/**
* Creates a small test file with specified name and extension.
*/
private File createTestFile(String fileName, int sizeInBytes) throws Exception {
File tempFile = File.createTempFile("test_", fileName);
try (FileWriter writer = new FileWriter(tempFile)) {
// Write some content to make it non-empty
for (int i = 0; i < sizeInBytes; i++) {
writer.write('A');
}
writer.flush();
}
return tempFile;
}
/**
* Creates a large test file for size limit testing.
*/
private File createLargeTestFile(String fileName, int sizeInBytes) throws Exception {
File tempFile = File.createTempFile("large_test_", fileName);
SecureRandom rng = new SecureRandom();
try (FileOutputStream fos = new FileOutputStream(tempFile)) {
byte[] buffer = new byte[8192];
int remaining = sizeInBytes;
while (remaining > 0) {
int toWrite = Math.min(buffer.length, remaining);
rng.nextBytes(buffer);
fos.write(buffer, 0, toWrite);
remaining -= toWrite;
}
fos.flush();
}
return tempFile;
}
}
@@ -1,183 +0,0 @@
/*
* 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 it.org.apache.struts2.showcase;
import org.htmlunit.WebClient;
import org.htmlunit.html.HtmlPage;
import org.junit.Assert;
import org.junit.Test;
/**
* Integration tests for HTML5 theme rendering in showcase application.
* <p>
* Tests validate that the HTML5 theme produces clean, semantic HTML5 markup
* without table-based layouts and properly displays action errors, action messages,
* and field errors.
*/
public class Html5TagExampleTest {
/**
* Tests basic HTML5 theme rendering and page load.
* <p>
* Verifies:
* - Page loads successfully (200 status)
* - HTML5 doctype is present
* - Page contains expected content
*/
@Test
public void testHtml5PageLoad() throws Exception {
try (final WebClient webClient = new WebClient()) {
webClient.getOptions().setThrowExceptionOnFailingStatusCode(false);
webClient.getOptions().setJavaScriptEnabled(false);
final HtmlPage page = webClient.getPage(
ParameterUtils.getBaseUrl() + "/html5/index.action"
);
Assert.assertEquals(200, page.getWebResponse().getStatusCode());
String pageContent = page.asNormalizedText();
Assert.assertTrue("Page should contain HTML5 demo title",
pageContent.contains("Html 5 tags demo"));
}
}
/**
* Tests HTML5 theme error and message display.
* <p>
* Verifies:
* - Action errors are displayed
* - Action messages are displayed
* - Field errors are displayed
* - Errors use clean HTML5 markup
*/
@Test
public void testHtml5ErrorDisplay() throws Exception {
try (final WebClient webClient = new WebClient()) {
webClient.getOptions().setThrowExceptionOnFailingStatusCode(false);
webClient.getOptions().setJavaScriptEnabled(false);
final HtmlPage page = webClient.getPage(
ParameterUtils.getBaseUrl() + "/html5/index.action"
);
String pageContent = page.asNormalizedText();
// Verify action error is displayed
Assert.assertTrue("Page should display action error",
pageContent.contains("Action error: only html5"));
// Verify action message is displayed
Assert.assertTrue("Page should display action message",
pageContent.contains("Action message: only html5"));
// Verify field error is displayed
Assert.assertTrue("Page should display field error",
pageContent.contains("Field error: only html5"));
}
}
/**
* Tests that HTML5 theme uses clean, semantic markup without tables.
* <p>
* Verifies:
* - No table-based layout for error messages
* - Uses semantic HTML5 elements
* - Error lists use &lt;ul&gt; elements
*/
@Test
public void testHtml5CleanMarkup() throws Exception {
try (final WebClient webClient = new WebClient()) {
webClient.getOptions().setThrowExceptionOnFailingStatusCode(false);
webClient.getOptions().setJavaScriptEnabled(false);
final HtmlPage page = webClient.getPage(
ParameterUtils.getBaseUrl() + "/html5/index.action"
);
String pageAsXml = page.asXml();
// HTML5 theme should use <ul> for error lists
Assert.assertTrue("Errors should be displayed in <ul> lists",
pageAsXml.contains("<ul"));
// Verify HTML5 theme does not use table-based layout for errors
Assert.assertFalse("HTML5 theme should not use table layout for errors",
pageAsXml.matches("(?s).*<table[^>]*>.*errorMessage.*</table>.*"));
}
}
/**
* Tests HTML5 theme anchor tag rendering.
* <p>
* Verifies:
* - Anchor tags are rendered correctly
* - Links have proper href attributes
* - HTML5 theme attributes are applied
*/
@Test
public void testHtml5AnchorTag() throws Exception {
try (final WebClient webClient = new WebClient()) {
webClient.getOptions().setJavaScriptEnabled(false);
final HtmlPage page = webClient.getPage(
ParameterUtils.getBaseUrl() + "/html5/index.action"
);
String pageContent = page.asNormalizedText();
// Verify anchor tag content is present
Assert.assertTrue("Page should contain 'index' link",
pageContent.contains("index"));
// Verify back link to showcase
Assert.assertTrue("Page should contain 'Back' link",
pageContent.contains("Back"));
}
}
/**
* Tests that HTML5 theme components are properly namespaced.
* <p>
* Verifies:
* - HTML5 action is accessible under /html5 namespace
* - Theme-specific rendering is applied
* - No conflicts with other themes
*/
@Test
public void testHtml5Namespace() throws Exception {
try (final WebClient webClient = new WebClient()) {
webClient.getOptions().setThrowExceptionOnFailingStatusCode(false);
final HtmlPage page = webClient.getPage(
ParameterUtils.getBaseUrl() + "/html5/index.action"
);
Assert.assertEquals("HTML5 action should return 200 status",
200, page.getWebResponse().getStatusCode());
String pageAsXml = page.asXml();
// Verify the page uses HTML5 theme by checking for theme-specific patterns
// HTML5 theme should not use table-based layouts
Assert.assertFalse("HTML5 theme should not use table layout for errors",
pageAsXml.matches("(?s).*<table[^>]*>.*errorMessage.*</table>.*"));
}
}
}
@@ -25,7 +25,7 @@ public class ParameterUtils {
public static String getBaseUrl() {
String port = System.getProperty("http.port");
if (port == null) {
port = "8090";
port = "8080";
}
return "http://localhost:"+port+"/struts2-showcase";
}
@@ -1,67 +0,0 @@
/*
* 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 it.org.apache.struts2.showcase;
import org.htmlunit.WebClient;
import org.htmlunit.html.HtmlPage;
import org.junit.Test;
import static org.junit.Assert.assertTrue;
/**
* Integration test verifying that Spring AOP proxied actions work correctly
* with action chaining. This tests the WW-5514 StrutsProxyService integration.
*
* <p>The test uses a Spring AOP proxied version of ActionChain1 (proxiedActionChain1)
* which is wrapped by {@link org.apache.struts2.showcase.proxy.LoggingInterceptor}.
* The ChainingInterceptor must correctly resolve the target class through
* StrutsProxyService to copy properties to the next action in the chain.</p>
*/
public class SpringProxyActionChainingTest {
/**
* Tests that action chaining works correctly when the first action is a Spring AOP proxy.
*
* <p>This verifies that:
* <ul>
* <li>StrutsProxyService correctly identifies the Spring CGLIB proxy</li>
* <li>ChainingInterceptor resolves the target class for property copying</li>
* <li>Properties from the proxied ActionChain1 are correctly copied to ActionChain2</li>
* </ul>
* </p>
*/
@Test
public void testProxiedActionChaining() throws Exception {
try (final WebClient webClient = new WebClient()) {
final HtmlPage page = webClient.getPage(
ParameterUtils.getBaseUrl() + "/actionchaining/proxiedActionChain1!input"
);
final String pageAsText = page.asNormalizedText();
// Verify properties were chained correctly despite proxy
assertTrue("ActionChain1 property should be present",
pageAsText.contains("Action Chain 1 Property 1: Property Set In Action Chain 1"));
assertTrue("ActionChain2 property should be present",
pageAsText.contains("Action Chain 2 Property 1: Property Set in Action Chain 2"));
assertTrue("ActionChain3 property should be present",
pageAsText.contains("Action Chain 3 Property 1: Property set in Action Chain 3"));
}
}
}
+5 -2
View File
@@ -24,7 +24,7 @@
<parent>
<groupId>org.apache.struts</groupId>
<artifactId>struts2-parent</artifactId>
<version>7.2.0</version>
<version>7.1.1</version>
<relativePath>../parent/pom.xml</relativePath>
</parent>
@@ -34,6 +34,9 @@
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.site.skip>true</maven.site.skip>
<maven.site.deploy.skip>true</maven.site.deploy.skip>
</properties>
<build>
@@ -104,7 +107,7 @@
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-assembly-plugin</artifactId>
<version>3.8.0</version>
<version>3.7.1</version>
<executions>
<execution>
<id>make-assembly</id>
+6 -1
View File
@@ -24,7 +24,7 @@
<parent>
<groupId>org.apache.struts</groupId>
<artifactId>struts2-project</artifactId>
<version>7.2.0</version>
<version>7.1.1</version>
</parent>
<artifactId>struts2-bom</artifactId>
@@ -32,6 +32,11 @@
<name>Struts BOM</name>
<description>Struts Bill of Materials (BOM)</description>
<properties>
<maven.site.skip>true</maven.site.skip>
<maven.site.deploy.skip>true</maven.site.deploy.skip>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
+3 -10
View File
@@ -24,7 +24,7 @@
<parent>
<groupId>org.apache.struts</groupId>
<artifactId>struts2-parent</artifactId>
<version>7.2.0</version>
<version>7.1.1</version>
<relativePath>../parent/pom.xml</relativePath>
</parent>
<artifactId>struts2-core</artifactId>
@@ -110,7 +110,7 @@
<arg>-AjspVersion=2.0</arg>
<arg>-AshortName=s</arg>
<arg>-AdisplayName=Struts Tags</arg>
<arg>-AoutFile=${project.build.outputDirectory}/META-INF/struts-tags.tld</arg>
<arg>-AoutFile=${basedir}/target/classes/META-INF/struts-tags.tld</arg>
<arg>-Adescription="To make it easier to access dynamic data;
the Apache Struts framework includes a library of custom tags.
The tags interact with the framework's validation and
@@ -118,15 +118,8 @@
to ensure that input is correct and output is localized.
The Struts Tags can be used with JSP FreeMarker or Velocity."
</arg>
<arg>-AoutTemplatesDir=${project.basedir}/src/site/resources/tags</arg>
<arg>-AoutTemplatesDir=${basedir}/src/site/resources/tags</arg>
</compilerArgs>
<annotationProcessorPaths>
<path>
<groupId>org.apache.struts</groupId>
<artifactId>struts-annotations</artifactId>
<version>${struts-annotations.version}</version>
</path>
</annotationProcessorPaths>
</configuration>
<executions>
<execution>
@@ -93,16 +93,9 @@ public interface ActionProxy {
String getMethod();
/**
* Returns whether the action method was explicitly specified rather than defaulting to {@code "execute"}.
* <p>
* This returns {@code true} when the method was provided via the URL (DMI), passed as a constructor argument,
* or resolved from the action configuration (including wildcard-substituted values like {@code method="{1}"}).
* It returns {@code false} only when no method was specified anywhere and the framework fell back
* to the default {@code "execute"} method.
* </p>
* Gets status of the method value's initialization.
*
* @return {@code true} if the method was explicitly provided or resolved from config;
* {@code false} only when defaulting to {@code "execute"}
* @return true if the method returned by getMethod() is not a default initializer value.
*/
boolean isMethodSpecified();
@@ -259,14 +259,7 @@ public class DefaultActionInvocation implements ActionInvocation {
final InterceptorMapping interceptorMapping = interceptors.next();
Interceptor interceptor = interceptorMapping.getInterceptor();
if (interceptor instanceof WithLazyParams) {
Map<String, String> params = interceptorMapping.getParams();
proxy.getConfig().getInterceptors().stream()
.filter(im -> im.getName().equals(interceptorMapping.getName()))
.findFirst()
.ifPresent(im -> params.putAll(im.getParams()));
interceptor = lazyParamInjector.injectParams(interceptor, params, invocationContext);
interceptor = lazyParamInjector.injectParams(interceptor, interceptorMapping.getParams(), invocationContext);
}
if (interceptor instanceof ConditionalInterceptor conditionalInterceptor) {
resultCode = executeConditional(conditionalInterceptor);
@@ -71,14 +71,14 @@ public class DefaultActionProxy implements ActionProxy, Serializable {
* <p>
* The reason for the builder methods is so that you can use a subclass to create your own DefaultActionProxy instance
* </p>
* <p>
*
* (like a RMIActionProxy).
*
* @param inv the action invocation
* @param namespace the namespace
* @param actionName the action name
* @param methodName the method name
* @param executeResult execute result
* @param inv the action invocation
* @param namespace the namespace
* @param actionName the action name
* @param methodName the method name
* @param executeResult execute result
* @param cleanupContext cleanup context
*/
protected DefaultActionProxy(ActionInvocation inv, String namespace, String actionName, String methodName, boolean executeResult, boolean cleanupContext) {
@@ -171,8 +171,8 @@ public class DefaultActionProxy implements ActionProxy, Serializable {
this.method = config.getMethodName();
if (StringUtils.isEmpty(this.method)) {
this.method = ActionConfig.DEFAULT_METHOD;
methodSpecified = false;
}
methodSpecified = false;
}
}
@@ -30,13 +30,8 @@ public interface Preparable {
/**
* This method is called to allow the action to prepare itself.
*
* <p>Default implementation is empty, allowing actions to implement only
* per-method variants like {@code prepareInput()}, {@code prepareEdit()}, etc.</p>
*
* @throws Exception thrown if a system level exception occurs.
*/
default void prepare() throws Exception {
// default empty implementation
}
void prepare() throws Exception;
}
@@ -256,15 +256,9 @@ public final class StrutsConstants {
public static final String STRUTS_OBJECTFACTORY_SPRING_AUTOWIRE = "struts.objectFactory.spring.autoWire";
/**
* Whether the autowire strategy chosen by STRUTS_OBJECTFACTORY_SPRING_AUTOWIRE is always respected.
* Defaults to true, which ensures the configured autowire strategy (AUTOWIRE_BY_NAME by default) is
* consistently used. This prevents issues where Spring's AUTOWIRE_CONSTRUCTOR strategy could inject
* unintended beans (e.g., String beans) into constructors.
* <p>
* Set to false to restore legacy behavior that mixes injection strategies, but be aware this can
* cause issues like WW-3647 where String beans are incorrectly injected into result class constructors.
* Whether the autowire strategy chosen by STRUTS_OBJECTFACTORY_SPRING_AUTOWIRE is always respected. Defaults
* to false, which is the legacy behavior that tries to determine the best strategy for the situation.
*
* @see <a href="https://issues.apache.org/jira/browse/WW-3647">WW-3647</a>
* @since 2.1.3
*/
public static final String STRUTS_OBJECTFACTORY_SPRING_AUTOWIRE_ALWAYS_RESPECT = "struts.objectFactory.spring.autoWire.alwaysRespect";
@@ -332,37 +326,6 @@ public final class StrutsConstants {
public static final String STRUTS_FREEMARKER_WRAPPER_ALT_MAP = "struts.freemarker.wrapper.altMap";
/**
* Controls FreeMarker whitespace stripping during template compilation.
* When enabled (default), removes indentation and trailing whitespace from lines containing only FTL tags.
* Automatically disabled when devMode is enabled.
*
* @since 7.2.0
*/
public static final String STRUTS_FREEMARKER_WHITESPACE_STRIPPING = "struts.freemarker.whitespaceStripping";
/**
* Controls whether the compress tag is enabled globally.
* When disabled, the compress tag will not compress output regardless of other settings.
*
* @since 7.2.0
*/
public static final String STRUTS_COMPRESS_ENABLED = "struts.tag.compress.enabled";
/**
* Maximum size (in bytes) of body content that can be compressed. Content exceeding this limit will be skipped without compression.
*
* @since 7.2.0
*/
public static final String STRUTS_COMPRESS_MAX_SIZE = "struts.tag.compress.maxSize";
/**
* Maximum length of body content to include in log messages. Content longer than this will be truncated with length indicator.
*
* @since 7.2.0
*/
public static final String STRUTS_COMPRESS_LOG_MAX_LENGTH = "struts.tag.compress.log.maxLength";
/**
* Extension point for the Struts CompoundRootAccessor
*/
@@ -522,50 +485,6 @@ public final class StrutsConstants {
*/
public static final String STRUTS_OGNL_EXPRESSION_CACHE_MAXSIZE = "struts.ognl.expressionCacheMaxSize";
/**
* Specifies the type of cache to use for proxy detection. Valid values defined in
* {@link org.apache.struts2.ognl.OgnlCacheFactory.CacheType}.
*
* @since 7.2.0
*/
public static final String STRUTS_PROXY_CACHE_TYPE = "struts.proxy.cacheType";
/**
* Specifies the maximum cache size for proxy detection caches.
*
* @since 7.2.0
*/
public static final String STRUTS_PROXY_CACHE_MAXSIZE = "struts.proxy.cacheMaxSize";
/**
* The {@link org.apache.struts2.ognl.ProxyCacheFactory} implementation class.
*
* @since 7.2.0
*/
public static final String STRUTS_PROXY_CACHE_FACTORY = "struts.proxy.cacheFactory";
/**
* The {@link org.apache.struts2.util.ProxyService} implementation class.
*
* @since 7.2.0
*/
public static final String STRUTS_PROXYSERVICE = "struts.proxyService";
/**
* The {@link org.apache.struts2.interceptor.parameter.ParameterAuthorizer} implementation class.
*
* @since 7.2.0
*/
public static final String STRUTS_PARAMETER_AUTHORIZER = "struts.parameterAuthorizer";
/**
* The {@link org.apache.struts2.interceptor.parameter.ParameterAllowlister} implementation class.
* Override to provide a custom allowlister for non-OGNL parameter targets.
*
* @since 7.2.0
*/
public static final String STRUTS_PARAMETER_ALLOWLISTER = "struts.parameterAllowlister";
/**
* Enables evaluation of OGNL expressions
*
@@ -612,7 +531,6 @@ public final class StrutsConstants {
public static final String STRUTS_CONVERTER_ANNOTATION_PROCESSOR = "struts.converter.annotation.processor";
public static final String STRUTS_CONVERTER_CREATOR = "struts.converter.creator";
public static final String STRUTS_CONVERTER_HOLDER = "struts.converter.holder";
public static final String STRUTS_CONVERTER_USER_PROPERTIES_PROVIDER = "struts.converter.userPropertiesProvider";
public static final String STRUTS_EXPRESSION_PARSER = "struts.expression.parser";
@@ -734,7 +652,6 @@ public final class StrutsConstants {
public static final String STRUTS_CHAINING_COPY_ERRORS = "struts.chaining.copyErrors";
public static final String STRUTS_CHAINING_COPY_FIELD_ERRORS = "struts.chaining.copyFieldErrors";
public static final String STRUTS_CHAINING_COPY_MESSAGES = "struts.chaining.copyMessages";
public static final String STRUTS_CHAINING_REQUIRE_ANNOTATIONS = "struts.chaining.requireAnnotations";
public static final String STRUTS_OBJECT_FACTORY_CLASSLOADER = "struts.objectFactory.classloader";
/**
@@ -752,15 +669,6 @@ public final class StrutsConstants {
*/
public static final String STRUTS_UI_CHECKBOX_SUBMIT_UNCHECKED = "struts.ui.checkbox.submitUnchecked";
/**
* The prefix used for hidden checkbox fields to track unchecked values.
* Default is "__checkbox_" for backward compatibility.
* Set to "struts_checkbox_" to avoid HTML validation warnings about double underscores.
*
* @since 7.2.0
*/
public static final String STRUTS_UI_CHECKBOX_HIDDEN_PREFIX = "struts.ui.checkbox.hiddenPrefix";
/**
* See {@link org.apache.struts2.interceptor.exec.ExecutorProvider}
*/
@@ -768,7 +676,6 @@ public final class StrutsConstants {
/**
* See {@link org.apache.struts2.interceptor.csp.CspNonceReader}
*
* @since 6.8.0
*/
public static final String STRUTS_CSP_NONCE_READER = "struts.csp.nonce.reader";
@@ -49,19 +49,17 @@ import jakarta.servlet.http.HttpServletResponse;
* </pre>
*/
@StrutsTag(
name = "checkbox",
tldTagClass = "org.apache.struts2.views.jsp.ui.CheckboxTag",
description = "Render a checkbox input field",
allowDynamicAttributes = true)
name = "checkbox",
tldTagClass = "org.apache.struts2.views.jsp.ui.CheckboxTag",
description = "Render a checkbox input field",
allowDynamicAttributes = true)
public class Checkbox extends UIBean {
private static final String ATTR_SUBMIT_UNCHECKED = "submitUnchecked";
private static final String ATTR_HIDDEN_PREFIX = "hiddenPrefix";
public static final String TEMPLATE = "checkbox";
private String submitUncheckedGlobal;
private String hiddenPrefixGlobal = "__checkbox_";
protected String fieldValue;
protected String submitUnchecked;
@@ -89,8 +87,6 @@ public class Checkbox extends UIBean {
} else {
addParameter(ATTR_SUBMIT_UNCHECKED, false);
}
addParameter(ATTR_HIDDEN_PREFIX, hiddenPrefixGlobal);
}
@Override
@@ -103,19 +99,14 @@ public class Checkbox extends UIBean {
this.submitUncheckedGlobal = submitUncheckedGlobal;
}
@Inject(value = StrutsConstants.STRUTS_UI_CHECKBOX_HIDDEN_PREFIX, required = false)
public void setHiddenPrefixGlobal(String hiddenPrefixGlobal) {
this.hiddenPrefixGlobal = hiddenPrefixGlobal;
}
@StrutsTagAttribute(description = "The actual HTML value attribute of the checkbox.", defaultValue = "true")
public void setFieldValue(String fieldValue) {
this.fieldValue = fieldValue;
}
@StrutsTagAttribute(description = "If set to true, unchecked elements will be submitted with the form. " +
"Since Struts 6.1.1 you can use a constant \"" + StrutsConstants.STRUTS_UI_CHECKBOX_SUBMIT_UNCHECKED + "\" to set this attribute globally",
type = "Boolean", defaultValue = "false")
"Since Struts 6.1.1 you can use a constant \"" + StrutsConstants.STRUTS_UI_CHECKBOX_SUBMIT_UNCHECKED + "\" to set this attribute globally",
type = "Boolean", defaultValue = "false")
public void setSubmitUnchecked(String submitUnchecked) {
this.submitUnchecked = submitUnchecked;
}
@@ -68,14 +68,6 @@ public class Component {
*/
protected static ConcurrentMap<Class<?>, Collection<String>> standardAttributesMap = new ConcurrentHashMap<>();
/**
* Clears the standard attributes cache to prevent classloader memory leaks during hot redeployment.
* The cache uses Class keys which pin the webapp classloader.
*/
public static void clearStandardAttributesMap() {
standardAttributesMap.clear();
}
protected boolean devMode = false;
protected boolean escapeHtmlBody = false;
protected ValueStack stack;
@@ -1,196 +0,0 @@
/*
* 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.struts2.components;
import org.apache.commons.lang3.BooleanUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.apache.struts2.StrutsConstants;
import org.apache.struts2.inject.Inject;
import org.apache.struts2.util.ValueStack;
import org.apache.struts2.views.annotations.StrutsTag;
import org.apache.struts2.views.annotations.StrutsTagAttribute;
import java.io.Writer;
/**
* <p>
* Used to compress HTML output. Just wrap a given section with the tag.
* </p>
*
* <p>
* <b>Security considerations:</b>
* </p>
* <ul>
* <li>Body content is truncated in log messages to prevent sensitive data exposure</li>
* <li>Maximum size limit prevents DoS attacks via large inputs (configurable via struts.tag.compress.maxSize)</li>
* <li>Regex operations include safeguards against ReDoS attacks</li>
* </ul>
*
* <p>
* Configurable attributes are:
* </p>
*
* <ul>
* <li>force (true/false) - always compress output, this can be useful in DevMode as devMode disables compression</li>
* </ul>
*
* <p><b>Examples</b></p>
* <pre>
* <!-- START SNIPPET: example -->
* &lt;s:compress&gt;
* &lt;s:form action="submit"&gt;
* &lt;s:text name="name" /&gt;
* ...
* &lt;/s:form&gt;
* &lt;/s:compress&gt;
* <!-- END SNIPPET: example -->
* </pre>
*
* <p>Uses conditional compression depending on action</p>
* <pre>
* <!-- START SNIPPET: example -->
* &lt;s:compress force="shouldCompress"&gt;
* &lt;s:form action="submit"&gt;
* &lt;s:text name="name" /&gt;
* ...
* &lt;/s:form&gt;
* &lt;/s:compress&gt;
* <!-- END SNIPPET: example -->
* </pre>
* "shouldCompress" is a field with getter define on action used in expression evaluation
*
* @since 7.2.0
*/
@StrutsTag(name = "compress", tldTagClass = "org.apache.struts2.views.jsp.CompressTag",
description = "Compress wrapped content\n\n<p><b>Security:</b> The compress tag includes built-in protections against DoS attacks and sensitive data exposure. Large content exceeding the configured maximum size (default 10MB) will be skipped without compression. Log messages are automatically truncated to prevent sensitive data from appearing in logs.</p>")
public class Compress extends Component {
private static final Logger LOG = LogManager.getLogger(Compress.class);
private String force;
private boolean compressionEnabled = true;
private Long maxSize = null;
private int logMaxLength = 200;
public Compress(ValueStack stack) {
super(stack);
}
@Inject(value = StrutsConstants.STRUTS_COMPRESS_ENABLED, required = false)
public void setCompressionEnabled(String compressionEnabled) {
this.compressionEnabled = BooleanUtils.toBoolean(compressionEnabled);
}
@Inject(value = StrutsConstants.STRUTS_COMPRESS_MAX_SIZE, required = false)
public void setMaxSize(String maxSize) {
try {
this.maxSize = Long.parseLong(maxSize.trim());
} catch (NumberFormatException e) {
this.maxSize = null;
}
}
@Inject(value = StrutsConstants.STRUTS_COMPRESS_LOG_MAX_LENGTH, required = false)
public void setLogMaxLength(String logMaxLength) {
try {
this.logMaxLength = Integer.parseInt(logMaxLength.trim());
} catch (NumberFormatException e) {
this.logMaxLength = 200;
}
}
@Override
public boolean end(Writer writer, String body) {
// Check size limit before processing
if (exceedsMaxSize(body) && compressionEnabled) {
LOG.warn("Body size: {} exceeds maximum allowed size: {}, skipping compression", body.length(), maxSize);
return super.end(writer, body, true);
}
Object forceValue = findValue(force, Boolean.class);
boolean forced = forceValue instanceof Boolean forcedValue && forcedValue;
if (!compressionEnabled && !forced) {
if (LOG.isDebugEnabled()) {
LOG.debug("Compression disabled globally, skipping: {}", truncateForLogging(body));
}
return super.end(writer, body, true);
}
if (devMode && !forced) {
if (LOG.isDebugEnabled()) {
LOG.debug("Avoids compressing output: {} in DevMode", truncateForLogging(body));
}
return super.end(writer, body, true);
}
if (LOG.isTraceEnabled()) {
LOG.trace("Compresses: {}", truncateForLogging(body));
}
String compressedBody = compressWhitespace(body);
if (LOG.isTraceEnabled()) {
LOG.trace("Compressed: {}", truncateForLogging(compressedBody));
}
return super.end(writer, compressedBody, true);
}
@Override
public boolean usesBody() {
return true;
}
@StrutsTagAttribute(description = "Force output compression")
public void setForce(String force) {
this.force = force;
}
private String truncateForLogging(String content) {
if (content == null) {
return null;
}
if (content.length() <= logMaxLength) {
return content;
}
return content.substring(0, logMaxLength) + "... (truncated, length: " + content.length() + ")";
}
private boolean exceedsMaxSize(String body) {
if (maxSize == null || body == null) {
return false;
}
return body.length() > maxSize;
}
private String compressWhitespace(String input) {
if (input == null || input.isEmpty()) {
return input;
}
// Early exit for very large inputs to prevent ReDoS and excessive processing
// This is a secondary check; primary size check happens in end() method
if (input.length() > 50_000_000) { // 50MB hard limit for regex operations
LOG.warn("Input size: {} exceeds safe processing limit (50MB), returning original content",
input.length());
return input;
}
// Simple compression: trim and remove whitespace between tags
return input.trim().replaceAll(">\\s+<", "><");
}
}
@@ -127,7 +127,6 @@ public class FreemarkerTemplateEngine extends BaseTemplateEngine {
model.put("tag", templateContext.getTag());
model.put("themeProperties", getThemeProps(templateContext.getTemplate()));
// the BodyContent JSP writer doesn't like it when FM flushes automatically --
// so let's just not do it (it will be flushed eventually anyway)
@@ -35,44 +35,7 @@ import org.apache.struts2.inject.Scope;
import java.util.Properties;
/**
* Base implementation of {@link BeanSelectionProvider} that provides bean aliasing functionality.
* <p>
* This class provides the {@link #alias(Class, String, ContainerBuilder, Properties, Scope)} method
* which is used to select and register bean implementations based on configuration properties.
* </p>
*
* <h2>Bean Selection Process</h2>
* <p>
* The {@code alias} method selects a bean implementation using the following process:
* </p>
* <ol>
* <li>Read the property value for the given key from the configuration properties</li>
* <li>If no property is set, use {@value #DEFAULT_BEAN_NAME} as the default bean name</li>
* <li>Check if a bean with that name already exists in the container:
* <ul>
* <li>If found, alias it to {@link Container#DEFAULT_NAME} making it the default</li>
* <li>If not found, try to load the property value as a fully qualified class name</li>
* </ul>
* </li>
* <li>If class loading succeeds, register the class as a factory for the interface type</li>
* <li>If class loading fails and the name is not the default, create a delegate factory
* that will resolve the bean through {@link ObjectFactory} at runtime. This allows
* Spring bean names to be used in configuration.</li>
* </ol>
*
* <h2>Usage Example</h2>
* <pre>
* // In struts.properties or struts.xml:
* // struts.objectFactory = spring
* // struts.converter.collection = myCustomCollectionConverter
*
* // In a subclass:
* alias(ObjectFactory.class, StrutsConstants.STRUTS_OBJECTFACTORY, builder, props);
* alias(CollectionConverter.class, StrutsConstants.STRUTS_CONVERTER_COLLECTION, builder, props);
* </pre>
*
* @see BeanSelectionProvider
* @see StrutsBeanSelectionProvider
* TODO lukaszlenart: write a JavaDoc
*/
public abstract class AbstractBeanSelectionProvider implements BeanSelectionProvider {
@@ -115,7 +78,7 @@ public abstract class AbstractBeanSelectionProvider implements BeanSelectionProv
// Perhaps a spring bean id, so we'll delegate to the object factory at runtime
LOG.trace("Choosing bean ({}) for ({}) to be loaded from the ObjectFactory", foundName, type.getName());
if (DEFAULT_BEAN_NAME.equals(foundName)) {
LOG.trace("No bean registered for type ({}) with default name '{}', skipping as optional", type.getName(), DEFAULT_BEAN_NAME);
// Probably an optional bean, will ignore
} else {
if (ObjectFactory.class != type) {
builder.factory(type, new ObjectFactoryDelegateFactory(foundName, type), scope);
@@ -145,7 +108,7 @@ public abstract class AbstractBeanSelectionProvider implements BeanSelectionProv
try {
return objFactory.buildBean(name, null, true);
} catch (ClassNotFoundException ex) {
throw new ConfigurationException(String.format("Unable to load bean %s (name = %s)", type.getName(), name));
throw new ConfigurationException("Unable to load bean "+type.getName()+" ("+name+")");
}
}
@@ -19,25 +19,7 @@
package org.apache.struts2.config;
/**
* A {@link ConfigurationProvider} that selects and aliases bean implementations.
* <p>
* Implementations of this interface are responsible for selecting which bean implementation
* to use for a given interface type. The selection is typically based on configuration properties
* that specify the bean name or class name.
* </p>
* <p>
* The aliasing mechanism works as follows:
* </p>
* <ol>
* <li>Look for a bean by the name specified in the configuration property</li>
* <li>If found, alias it to the default name so it becomes the default implementation</li>
* <li>If not found, try to load the value as a class name and register it as a factory</li>
* <li>If class loading fails, delegate to {@link org.apache.struts2.ObjectFactory} at runtime
* (useful for Spring bean names)</li>
* </ol>
*
* @see AbstractBeanSelectionProvider
* @see StrutsBeanSelectionProvider
* When implemented allows to alias already existing beans
*/
public interface BeanSelectionProvider extends ConfigurationProvider {
@@ -37,7 +37,6 @@ import org.apache.struts2.conversion.ConversionPropertiesProcessor;
import org.apache.struts2.conversion.ObjectTypeDeterminer;
import org.apache.struts2.conversion.TypeConverterCreator;
import org.apache.struts2.conversion.TypeConverterHolder;
import org.apache.struts2.conversion.UserConversionPropertiesProvider;
import org.apache.struts2.conversion.impl.ArrayConverter;
import org.apache.struts2.conversion.impl.CollectionConverter;
import org.apache.struts2.conversion.impl.DateConverter;
@@ -61,7 +60,6 @@ import org.apache.struts2.interceptor.exec.ExecutorProvider;
import org.apache.struts2.ognl.BeanInfoCacheFactory;
import org.apache.struts2.ognl.ExpressionCacheFactory;
import org.apache.struts2.ognl.OgnlGuard;
import org.apache.struts2.ognl.ProxyCacheFactory;
import org.apache.struts2.ognl.SecurityMemberAccess;
import org.apache.struts2.ognl.accessor.RootAccessor;
import org.apache.struts2.security.AcceptedPatternsChecker;
@@ -73,9 +71,6 @@ import org.apache.struts2.url.UrlDecoder;
import org.apache.struts2.url.UrlEncoder;
import org.apache.struts2.util.ContentTypeMatcher;
import org.apache.struts2.util.PatternMatcher;
import org.apache.struts2.interceptor.parameter.ParameterAllowlister;
import org.apache.struts2.interceptor.parameter.ParameterAuthorizer;
import org.apache.struts2.util.ProxyService;
import org.apache.struts2.util.TextParser;
import org.apache.struts2.util.ValueStackFactory;
import org.apache.struts2.util.location.LocatableProperties;
@@ -93,7 +88,7 @@ import org.apache.struts2.views.util.UrlHelper;
*
* <p>
* The following is a list of the allowed extension points:
* <p>
*
* <!-- START SNIPPET: extensionPoints -->
* <table border="1" summary="">
* <tr>
@@ -358,7 +353,7 @@ import org.apache.struts2.views.util.UrlHelper;
* <td>Provides access to resource bundles used to localise messages (since 2.5.11)</td>
* </tr>
* </table>
* <p>
*
* <!-- END SNIPPET: extensionPoints -->
*
* <p>
@@ -410,7 +405,6 @@ public class StrutsBeanSelectionProvider extends AbstractBeanSelectionProvider {
alias(ConversionAnnotationProcessor.class, StrutsConstants.STRUTS_CONVERTER_ANNOTATION_PROCESSOR, builder, props);
alias(TypeConverterCreator.class, StrutsConstants.STRUTS_CONVERTER_CREATOR, builder, props);
alias(TypeConverterHolder.class, StrutsConstants.STRUTS_CONVERTER_HOLDER, builder, props);
alias(UserConversionPropertiesProvider.class, StrutsConstants.STRUTS_CONVERTER_USER_PROPERTIES_PROVIDER, builder, props);
alias(TextProvider.class, StrutsConstants.STRUTS_TEXT_PROVIDER, builder, props, Scope.PROTOTYPE);
alias(TextProviderFactory.class, StrutsConstants.STRUTS_TEXT_PROVIDER_FACTORY, builder, props, Scope.PROTOTYPE);
@@ -446,10 +440,6 @@ public class StrutsBeanSelectionProvider extends AbstractBeanSelectionProvider {
alias(ExpressionCacheFactory.class, StrutsConstants.STRUTS_OGNL_EXPRESSION_CACHE_FACTORY, builder, props, Scope.SINGLETON);
alias(BeanInfoCacheFactory.class, StrutsConstants.STRUTS_OGNL_BEANINFO_CACHE_FACTORY, builder, props, Scope.SINGLETON);
alias(ProxyCacheFactory.class, StrutsConstants.STRUTS_PROXY_CACHE_FACTORY, builder, props, Scope.SINGLETON);
alias(ProxyService.class, StrutsConstants.STRUTS_PROXYSERVICE, builder, props, Scope.SINGLETON);
alias(ParameterAuthorizer.class, StrutsConstants.STRUTS_PARAMETER_AUTHORIZER, builder, props, Scope.SINGLETON);
alias(ParameterAllowlister.class, StrutsConstants.STRUTS_PARAMETER_ALLOWLISTER, builder, props, Scope.SINGLETON);
alias(SecurityMemberAccess.class, StrutsConstants.STRUTS_MEMBER_ACCESS, builder, props, Scope.PROTOTYPE);
alias(OgnlGuard.class, StrutsConstants.STRUTS_OGNL_GUARD, builder, props, Scope.SINGLETON);
@@ -85,21 +85,13 @@ import org.apache.struts2.ognl.ExpressionCacheFactory;
import org.apache.struts2.ognl.OgnlCacheFactory;
import org.apache.struts2.ognl.OgnlReflectionProvider;
import org.apache.struts2.ognl.OgnlUtil;
import org.apache.struts2.ognl.ProxyCacheFactory;
import org.apache.struts2.ognl.StrutsProxyCacheFactory;
import org.apache.struts2.ognl.OgnlValueStackFactory;
import org.apache.struts2.ognl.SecurityMemberAccess;
import org.apache.struts2.ognl.accessor.CompoundRootAccessor;
import org.apache.struts2.ognl.accessor.RootAccessor;
import org.apache.struts2.ognl.accessor.XWorkMethodAccessor;
import org.apache.struts2.interceptor.parameter.OgnlParameterAllowlister;
import org.apache.struts2.interceptor.parameter.ParameterAllowlister;
import org.apache.struts2.interceptor.parameter.StrutsParameterAuthorizer;
import org.apache.struts2.interceptor.parameter.ParameterAuthorizer;
import org.apache.struts2.util.StrutsProxyService;
import org.apache.struts2.util.OgnlTextParser;
import org.apache.struts2.util.PatternMatcher;
import org.apache.struts2.util.ProxyService;
import org.apache.struts2.text.StrutsLocalizedTextProvider;
import org.apache.struts2.util.TextParser;
import org.apache.struts2.util.ValueStack;
@@ -114,8 +106,6 @@ import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.apache.struts2.StrutsConstants;
import org.apache.struts2.conversion.StrutsConversionPropertiesProcessor;
import org.apache.struts2.conversion.UserConversionPropertiesProcessor;
import org.apache.struts2.conversion.UserConversionPropertiesProvider;
import org.apache.struts2.conversion.StrutsTypeConverterCreator;
import org.apache.struts2.conversion.StrutsTypeConverterHolder;
import org.apache.struts2.factory.StrutsResultFactory;
@@ -136,8 +126,12 @@ import java.util.Set;
import java.util.TreeMap;
import java.util.TreeSet;
/**
* DefaultConfiguration
*
* @author Jason Carreira
* Created Feb 24, 2003 7:38:06 AM
*/
public class DefaultConfiguration implements Configuration {
@@ -152,8 +146,6 @@ public class DefaultConfiguration implements Configuration {
constants.put(StrutsConstants.STRUTS_OGNL_EXPRESSION_CACHE_MAXSIZE, 10000);
constants.put(StrutsConstants.STRUTS_OGNL_BEANINFO_CACHE_TYPE, OgnlCacheFactory.CacheType.BASIC);
constants.put(StrutsConstants.STRUTS_OGNL_BEANINFO_CACHE_MAXSIZE, 10000);
constants.put(StrutsConstants.STRUTS_PROXY_CACHE_TYPE, OgnlCacheFactory.CacheType.BASIC);
constants.put(StrutsConstants.STRUTS_PROXY_CACHE_MAXSIZE, 10000);
constants.put(StrutsConstants.STRUTS_ENABLE_DYNAMIC_METHOD_INVOCATION, Boolean.FALSE);
BOOTSTRAP_CONSTANTS = Collections.unmodifiableMap(constants);
}
@@ -233,7 +225,7 @@ public class DefaultConfiguration implements Configuration {
name, packageContext.getLocation());
} else {
throw new ConfigurationException("The package name '" + name
+ "' at location " + packageContext.getLocation()
+ "' at location "+packageContext.getLocation()
+ " is already been used by another package at location " + check.getLocation(),
packageContext);
}
@@ -253,10 +245,6 @@ public class DefaultConfiguration implements Configuration {
public void destroy() {
packageContexts.clear();
loadedFileNames.clear();
if (container != null) {
container.destroy();
container = null;
}
}
@Override
@@ -270,24 +258,27 @@ public class DefaultConfiguration implements Configuration {
*
* @param providers list of ContainerProvider
* @return list of package providers
*
* @throws ConfigurationException in case of any configuration errors
*/
@Override
public synchronized List<PackageProvider> reloadContainer(List<ContainerProvider> providers) throws ConfigurationException {
destroy();
packageContexts.clear();
loadedFileNames.clear();
List<PackageProvider> packageProviders = new ArrayList<>();
ContainerProperties props = new ContainerProperties();
ContainerBuilder builder = new ContainerBuilder();
Container bootstrap = createBootstrapContainer(providers);
for (final ContainerProvider containerProvider : providers) {
for (final ContainerProvider containerProvider : providers)
{
bootstrap.inject(containerProvider);
containerProvider.init(this);
containerProvider.register(builder, props);
}
props.setConstants(builder);
builder.factory(Configuration.class, new Factory<>() {
builder.factory(Configuration.class, new Factory<Configuration>() {
@Override
public Configuration create(Context context) throws Exception {
return DefaultConfiguration.this;
@@ -308,16 +299,13 @@ public class DefaultConfiguration implements Configuration {
setContext(container);
objectFactory = container.getInstance(ObjectFactory.class);
// Trigger late initialization of user conversion properties (WW-4291)
// This must happen after full container is built so SpringObjectFactory is available
container.getInstance(UserConversionPropertiesProcessor.class);
// Process the configuration providers first
for (final ContainerProvider containerProvider : providers) {
for (final ContainerProvider containerProvider : providers)
{
if (containerProvider instanceof PackageProvider) {
container.inject(containerProvider);
((PackageProvider) containerProvider).loadPackages();
packageProviders.add((PackageProvider) containerProvider);
((PackageProvider)containerProvider).loadPackages();
packageProviders.add((PackageProvider)containerProvider);
}
}
@@ -393,8 +381,6 @@ public class DefaultConfiguration implements Configuration {
.factory(ConversionAnnotationProcessor.class, DefaultConversionAnnotationProcessor.class, Scope.SINGLETON)
.factory(TypeConverterCreator.class, StrutsTypeConverterCreator.class, Scope.SINGLETON)
.factory(TypeConverterHolder.class, StrutsTypeConverterHolder.class, Scope.SINGLETON)
.factory(UserConversionPropertiesProvider.class, StrutsConversionPropertiesProcessor.class, Scope.SINGLETON)
.factory(UserConversionPropertiesProcessor.class, Scope.SINGLETON)
.factory(TextProvider.class, "system", DefaultTextProvider.class, Scope.SINGLETON)
.factory(LocalizedTextProvider.class, StrutsLocalizedTextProvider.class, Scope.SINGLETON)
@@ -408,10 +394,6 @@ public class DefaultConfiguration implements Configuration {
.factory(ExpressionCacheFactory.class, DefaultOgnlExpressionCacheFactory.class, Scope.SINGLETON)
.factory(BeanInfoCacheFactory.class, DefaultOgnlBeanInfoCacheFactory.class, Scope.SINGLETON)
.factory(ProxyCacheFactory.class, StrutsProxyCacheFactory.class, Scope.SINGLETON)
.factory(ProxyService.class, StrutsProxyService.class, Scope.SINGLETON)
.factory(ParameterAuthorizer.class, StrutsParameterAuthorizer.class, Scope.SINGLETON)
.factory(ParameterAllowlister.class, OgnlParameterAllowlister.class, Scope.SINGLETON)
.factory(OgnlUtil.class, Scope.SINGLETON)
.factory(SecurityMemberAccess.class, Scope.PROTOTYPE)
.factory(OgnlGuard.class, StrutsOgnlGuard.class, Scope.SINGLETON)
@@ -462,9 +444,10 @@ public class DefaultConfiguration implements Configuration {
Map<String, ActionConfig> actionConfigs = packageConfig.getAllActionConfigs();
for (Map.Entry<String, ActionConfig> entry : actionConfigs.entrySet()) {
ActionConfig baseConfig = entry.getValue();
configs.put(entry.getKey(), buildFullActionConfig(packageConfig, baseConfig));
for (Object o : actionConfigs.keySet()) {
String actionName = (String) o;
ActionConfig baseConfig = actionConfigs.get(actionName);
configs.put(actionName, buildFullActionConfig(packageConfig, baseConfig));
}
namespaceActionConfigs.put(namespace, configs);
@@ -506,6 +489,7 @@ public class DefaultConfiguration implements Configuration {
* and inheritance
* @return a full ActionConfig for runtime configuration with all of the inherited and default params
* @throws org.apache.struts2.config.ConfigurationException
*
*/
private ActionConfig buildFullActionConfig(PackageConfig packageContext, ActionConfig baseConfig) throws ConfigurationException {
Map<String, String> params = new TreeMap<>(baseConfig.getParams());
@@ -517,7 +501,7 @@ public class DefaultConfiguration implements Configuration {
results.putAll(packageContext.getAllGlobalResults());
}
results.putAll(baseConfig.getResults());
results.putAll(baseConfig.getResults());
setDefaultResults(results, packageContext);
@@ -528,7 +512,7 @@ public class DefaultConfiguration implements Configuration {
if (defaultInterceptorRefName != null) {
interceptors.addAll(InterceptorBuilder.constructInterceptorReference(new PackageConfig.Builder(packageContext), defaultInterceptorRefName,
new LinkedHashMap<>(), packageContext.getLocation(), objectFactory));
new LinkedHashMap<String, String>(), packageContext.getLocation(), objectFactory));
}
}
@@ -540,14 +524,14 @@ public class DefaultConfiguration implements Configuration {
LOG.debug("Using pattern [{}] to match allowed methods when SMI is disabled!", methodRegex);
return new ActionConfig.Builder(baseConfig)
.addParams(params)
.addResultConfigs(results)
.defaultClassName(packageContext.getDefaultClassRef()) // fill in default if non class has been provided
.interceptors(interceptors)
.setStrictMethodInvocation(packageContext.isStrictMethodInvocation())
.setDefaultMethodRegex(methodRegex)
.addExceptionMappings(packageContext.getAllExceptionMappingConfigs())
.build();
.addParams(params)
.addResultConfigs(results)
.defaultClassName(packageContext.getDefaultClassRef()) // fill in default if non class has been provided
.interceptors(interceptors)
.setStrictMethodInvocation(packageContext.isStrictMethodInvocation())
.setDefaultMethodRegex(methodRegex)
.addExceptionMappings(packageContext.getAllExceptionMappingConfigs())
.build();
}
@@ -563,7 +547,8 @@ public class DefaultConfiguration implements Configuration {
Map<String, String> namespaceConfigs,
PatternMatcher<int[]> matcher,
boolean appendNamedParameters,
boolean fallbackToEmptyNamespace) {
boolean fallbackToEmptyNamespace)
{
this.namespaceActionConfigs = namespaceActionConfigs;
this.namespaceConfigs = namespaceConfigs;
this.fallbackToEmptyNamespace = fallbackToEmptyNamespace;
@@ -618,39 +603,26 @@ public class DefaultConfiguration implements Configuration {
}
private ActionConfig findActionConfigInNamespace(String namespace, String name) {
ActionConfig config = null;
if (namespace == null) {
namespace = "";
}
Map<String, ActionConfig> actions = namespaceActionConfigs.get(namespace);
if (actions == null) {
return null;
if (actions != null) {
config = actions.get(name);
// Check wildcards
if (config == null) {
config = namespaceActionConfigMatchers.get(namespace).match(name);
// fail over to default action
if (config == null) {
String defaultActionRef = namespaceConfigs.get(namespace);
if (defaultActionRef != null) {
config = actions.get(defaultActionRef);
}
}
}
}
ActionConfig config = actions.get(name);
if (config != null) {
return config;
}
config = namespaceActionConfigMatchers.get(namespace).match(name);
if (config != null) {
return config;
}
return findDefaultActionConfig(namespace, actions);
}
private ActionConfig findDefaultActionConfig(String namespace, Map<String, ActionConfig> actions) {
String defaultActionRef = namespaceConfigs.get(namespace);
if (defaultActionRef == null) {
return null;
}
ActionConfig config = actions.get(defaultActionRef);
if (config != null) {
return config;
}
return namespaceActionConfigMatchers.get(namespace).match(defaultActionRef);
return config;
}
/**
@@ -659,7 +631,7 @@ public class DefaultConfiguration implements Configuration {
* @return a Map of namespace - > Map of ActionConfig objects, with the key being the action name
*/
@Override
public Map<String, Map<String, ActionConfig>> getActionConfigs() {
public Map<String, Map<String, ActionConfig>> getActionConfigs() {
return namespaceActionConfigs;
}
@@ -694,7 +666,7 @@ public class DefaultConfiguration implements Configuration {
public void setConstants(ContainerBuilder builder) {
for (Object keyobj : keySet()) {
String key = (String) keyobj;
String key = (String)keyobj;
builder.factory(String.class, key, new LocatableConstantFactory<>(getProperty(key), getPropertyLocation(key)));
}
}
@@ -51,16 +51,16 @@ public class InterceptorBuilder {
* Builds a list of interceptors referenced by the refName in the supplied PackageConfig (InterceptorMapping object).
*
* @param interceptorLocator interceptor locator
* @param refName reference name
* @param refParams reference parameters
* @param location location
* @param objectFactory object factory
* @param refName reference name
* @param refParams reference parameters
* @param location location
* @param objectFactory object factory
* @return list of interceptors referenced by the refName in the supplied PackageConfig (InterceptorMapping object).
* @throws ConfigurationException in case of any configuration errors
*/
public static List<InterceptorMapping> constructInterceptorReference(InterceptorLocator interceptorLocator,
String refName,
Map<String, String> refParams,
Map<String,String> refParams,
Location location,
ObjectFactory objectFactory) throws ConfigurationException {
Object referencedConfig = interceptorLocator.getInterceptorConfig(refName);
@@ -70,14 +70,22 @@ public class InterceptorBuilder {
throw new ConfigurationException("Unable to find interceptor class referenced by ref-name " + refName, location);
} else {
if (referencedConfig instanceof InterceptorConfig config) {
Interceptor inter = objectFactory.buildInterceptor(config, refParams);
result.add(new InterceptorMapping(refName, inter, refParams));
try {
Interceptor inter = objectFactory.buildInterceptor(config, refParams);
result.add(new InterceptorMapping(refName, inter, refParams));
} catch (ConfigurationException ex) {
LOG.warn(new ParameterizedMessage("Unable to load config class {} at {} probably due to a missing jar, which might be fine if you never plan to use the {} interceptor",
config.getClassName(), ex.getLocation(), config.getName()), ex);
}
} else if (referencedConfig instanceof InterceptorStackConfig stackConfig) {
if (refParams != null && !refParams.isEmpty()) {
result = constructParameterizedInterceptorReferences(interceptorLocator, stackConfig, refParams, objectFactory);
} else {
result.addAll(stackConfig.getInterceptors());
}
} else {
LOG.error("Got unexpected type for interceptor {}. Got {}", refName, referencedConfig);
}
@@ -91,14 +99,14 @@ public class InterceptorBuilder {
* of the referenced interceptor with refParams.
*
* @param interceptorLocator interceptor locator
* @param stackConfig interceptor stack configuration
* @param refParams The overridden interceptor properties
* @param stackConfig interceptor stack configuration
* @param refParams The overridden interceptor properties
* @return list of interceptors referenced by the refName in the supplied PackageConfig overridden with refParams.
*/
private static List<InterceptorMapping> constructParameterizedInterceptorReferences(
InterceptorLocator interceptorLocator,
InterceptorStackConfig stackConfig,
Map<String, String> refParams,
Map<String,String> refParams,
ObjectFactory objectFactory) {
List<InterceptorMapping> result;
Map<String, Map<String, String>> params = new LinkedHashMap<>();
@@ -174,7 +182,7 @@ public class InterceptorBuilder {
if (interceptorCfgObj instanceof InterceptorConfig cfg) { // interceptor-ref param refer to an interceptor
Interceptor interceptor = objectFactory.buildInterceptor(cfg, map);
InterceptorMapping mapping = new InterceptorMapping(key, interceptor, map);
InterceptorMapping mapping = new InterceptorMapping(key, interceptor);
if (result.contains(mapping)) {
for (int index = 0; index < result.size(); index++) {
InterceptorMapping interceptorMapping = result.get(index);
@@ -186,7 +194,8 @@ public class InterceptorBuilder {
} else {
result.add(mapping);
}
} else if (interceptorCfgObj instanceof InterceptorStackConfig stackCfg) { // interceptor-ref param refer to an interceptor stack
} else
if (interceptorCfgObj instanceof InterceptorStackConfig stackCfg) { // interceptor-ref param refer to an interceptor stack
// If its an interceptor-stack, we call this method recursively until,
// all the params (eg. interceptorStack1.interceptor1.param etc.)
@@ -31,7 +31,7 @@ import java.util.Iterator;
import java.util.Map;
import java.util.Properties;
public class StrutsConversionPropertiesProcessor implements ConversionPropertiesProcessor, EarlyInitializable, UserConversionPropertiesProvider {
public class StrutsConversionPropertiesProcessor implements ConversionPropertiesProcessor, EarlyInitializable {
private static final Logger LOG = LogManager.getLogger(StrutsConversionPropertiesProcessor.class);
@@ -54,27 +54,8 @@ public class StrutsConversionPropertiesProcessor implements ConversionProperties
@Override
public void init() {
// Early phase: Only process framework defaults (class names only)
// User properties are processed later in initUserConversions() when
// SpringObjectFactory is available for bean name resolution (WW-4291)
LOG.debug("Processing default conversion properties files (early phase)");
LOG.debug("Processing default conversion properties files");
processRequired(STRUTS_DEFAULT_CONVERSION_PROPERTIES);
}
/**
* Process user conversion properties. Called during late initialization
* when SpringObjectFactory is available for bean name resolution.
* <p>
* This allows users to reference Spring bean names in struts-conversion.properties
* instead of only fully qualified class names.
* </p>
*
* @see <a href="https://issues.apache.org/jira/browse/WW-4291">WW-4291</a>
* @since 7.2.0
*/
@Override
public void initUserConversions() {
LOG.debug("Processing user conversion properties files (late phase)");
process(STRUTS_CONVERSION_PROPERTIES);
process(XWORK_CONVERSION_PROPERTIES);
}
@@ -93,7 +74,7 @@ public class StrutsConversionPropertiesProcessor implements ConversionProperties
while (resources.hasNext()) {
if (XWORK_CONVERSION_PROPERTIES.equals(propsName)) {
LOG.warn("Instead of using deprecated {} please use the new file name {}",
XWORK_CONVERSION_PROPERTIES, STRUTS_CONVERSION_PROPERTIES);
XWORK_CONVERSION_PROPERTIES, STRUTS_CONVERSION_PROPERTIES);
}
URL url = resources.next();
Properties props = new Properties();
@@ -101,7 +82,8 @@ public class StrutsConversionPropertiesProcessor implements ConversionProperties
LOG.debug("Processing conversion file [{}]", propsName);
for (Map.Entry<Object, Object> entry : props.entrySet()) {
for (Object o : props.entrySet()) {
Map.Entry entry = (Map.Entry) o;
String key = (String) entry.getKey();
try {
@@ -1,55 +0,0 @@
/*
* 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.struts2.conversion;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.apache.struts2.inject.Initializable;
import org.apache.struts2.inject.Inject;
/**
* Late initialization processor for user conversion properties.
* <p>
* Processes struts-conversion.properties and xwork-conversion.properties
* after the full container is built, allowing Spring bean name resolution.
* This enables users to reference Spring bean names instead of only fully
* qualified class names in their conversion property files.
* </p>
*
* @see <a href="https://issues.apache.org/jira/browse/WW-4291">WW-4291</a>
* @see UserConversionPropertiesProvider
* @since 7.2.0
*/
public class UserConversionPropertiesProcessor implements Initializable {
private static final Logger LOG = LogManager.getLogger(UserConversionPropertiesProcessor.class);
private UserConversionPropertiesProvider provider;
@Inject
public void setUserConversionPropertiesProvider(UserConversionPropertiesProvider provider) {
this.provider = provider;
}
@Override
public void init() {
LOG.debug("Initializing user conversion properties via late initialization");
provider.initUserConversions();
}
}
@@ -1,38 +0,0 @@
/*
* 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.struts2.conversion;
/**
* Interface for processors that support late initialization of user conversion properties.
* <p>
* Implementations provide user conversion properties processing after the full container
* is built, allowing Spring bean name resolution for type converters.
* </p>
*
* @see <a href="https://issues.apache.org/jira/browse/WW-4291">WW-4291</a>
* @since 7.2.0
*/
public interface UserConversionPropertiesProvider {
/**
* Process user conversion properties (struts-conversion.properties, xwork-conversion.properties).
* Called during late initialization when SpringObjectFactory is available.
*/
void initUserConversions();
}
@@ -1,35 +0,0 @@
/*
* 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.struts2.dispatcher;
import org.apache.struts2.components.Component;
/**
* Clears {@link Component}'s static standard attributes cache to prevent
* classloader leaks on hot redeploy.
*
* @since 7.2.0
*/
public class ComponentCacheDestroyable implements InternalDestroyable {
@Override
public void destroy() {
Component.clearStandardAttributesMap();
}
}
@@ -20,65 +20,28 @@ package org.apache.struts2.dispatcher;
import org.apache.struts2.inject.Container;
import java.util.concurrent.atomic.AtomicLong;
/**
* Per-thread cache for the Container instance, minimising repeated reads from
* {@link org.apache.struts2.config.ConfigurationManager}.
* Simple class to hold Container instance per thread to minimise number of attempts
* to read configuration and build each time a new configuration.
* <p>
* WW-5537: Uses a ThreadLocal for per-request isolation with an AtomicLong generation
* counter for cross-thread invalidation during app undeploy. When
* {@link #invalidateAll()} is called, all threads see the updated generation on their
* next {@link #get()} and return {@code null}, forcing a fresh read from
* ConfigurationManager. This prevents classloader leaks caused by idle pool threads
* retaining stale Container references after hot redeployment.
* As ContainerHolder operates just per thread (which means per request) there is no need
* to check if configuration changed during the same request. If changed between requests,
* first call to store Container in ContainerHolder will be with the new configuration.
*/
class ContainerHolder {
private static final ThreadLocal<CachedContainer> instance = new ThreadLocal<>();
/**
* Incremented on each {@link #invalidateAll()} call. Threads compare their cached
* generation against this value to detect staleness.
*/
private static final AtomicLong generation = new AtomicLong();
private static final ThreadLocal<Container> instance = new ThreadLocal<>();
public static void store(Container newInstance) {
instance.set(new CachedContainer(newInstance, generation.get()));
instance.set(newInstance);
}
public static Container get() {
CachedContainer cached = instance.get();
if (cached == null) {
return null;
}
if (cached.generation() != generation.get()) {
instance.remove();
return null;
}
return cached.container();
return instance.get();
}
/**
* Clears the current thread's cached container reference.
* Used for per-request cleanup.
*/
public static void clear() {
instance.remove();
}
/**
* Invalidates all threads' cached container references by advancing the generation
* counter. Each thread will detect the stale generation on its next {@link #get()}
* call and clear its own ThreadLocal. Also clears the calling thread immediately.
* <p>
* Used during application undeploy ({@link Dispatcher#cleanup()}) to ensure idle
* pool threads do not pin the webapp classloader via retained Container references.
*/
public static void invalidateAll() {
generation.incrementAndGet();
instance.remove();
}
private record CachedContainer(Container container, long generation) {}
}
@@ -1,53 +0,0 @@
/*
* 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.struts2.dispatcher;
import jakarta.servlet.ServletContext;
/**
* Extension of {@link InternalDestroyable} for components that require
* {@link ServletContext} during cleanup (e.g. clearing servlet-scoped caches).
*
* &lt;p&gt;During {@link Dispatcher#cleanup()}, the discovery loop checks each
* {@code InternalDestroyable} bean: if it implements this subinterface,
* {@link #destroy(ServletContext)} is called instead of {@link #destroy()}.&lt;/p&gt;
*
* @since 7.2.0
* @see InternalDestroyable
* @see Dispatcher#cleanup()
*/
public interface ContextAwareDestroyable extends InternalDestroyable {
/**
* Releases state that requires access to the {@link ServletContext}.
*
* @param servletContext the current servlet context, may be {@code null}
* if the Dispatcher was created without one
*/
void destroy(ServletContext servletContext);
/**
* Default no-op — {@link Dispatcher} calls
* {@link #destroy(ServletContext)} instead when it recognises this type.
*/
@Override
default void destroy() {
// no-op: context-aware variant is the real entry point
}
}
@@ -1,35 +0,0 @@
/*
* 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.struts2.dispatcher;
import org.apache.struts2.util.DebugUtils;
/**
* Clears {@link DebugUtils}'s static logged-keys cache to prevent memory leaks
* during hot redeployment.
*
* @since 7.2.0
*/
public class DebugUtilsCacheDestroyable implements InternalDestroyable {
@Override
public void destroy() {
DebugUtils.clearCache();
}
}
@@ -441,78 +441,37 @@ public class Dispatcher {
* Releases all instances bound to this dispatcher instance.
*/
public void cleanup() {
destroyObjectFactory();
// clean up ObjectFactory
if (objectFactory == null) {
LOG.warn("Object Factory is null, something is seriously wrong, no clean up will be performed");
}
if (objectFactory instanceof ObjectFactoryDestroyable) {
try {
((ObjectFactoryDestroyable) objectFactory).destroy();
} catch (Exception e) {
// catch any exception that may occur during destroy() and log it
LOG.error("Exception occurred while destroying ObjectFactory [{}]", objectFactory.toString(), e);
}
}
// clean up Dispatcher itself for this thread
instance.remove();
servletContext.setAttribute(StrutsStatics.SERVLET_DISPATCHER, null);
destroyDispatcherListeners();
destroyInterceptors();
destroyInternalBeans();
// WW-5537: Invalidate all threads' cached Container references to prevent
// classloader leaks from idle pool threads retaining stale references after undeploy.
ContainerHolder.invalidateAll();
//cleanup action context
ActionContext.clear();
// clean up configuration
configurationManager.destroyConfiguration();
configurationManager = null;
}
/**
* Destroys the {@link ObjectFactory} if it implements {@link ObjectFactoryDestroyable}.
*
* @since 7.2.0
*/
protected void destroyObjectFactory() {
if (objectFactory == null) {
LOG.warn("Object Factory is null, something is seriously wrong, no clean up will be performed");
return;
}
if (objectFactory instanceof ObjectFactoryDestroyable ofd) {
try {
ofd.destroy();
} catch (Exception e) {
LOG.error("Exception occurred while destroying ObjectFactory [{}]", objectFactory.toString(), e);
}
}
}
/**
* Notifies all registered {@link DispatcherListener}s that this dispatcher
* is being destroyed, then clears the listener list.
*
* @since 7.2.0
*/
protected void destroyDispatcherListeners() {
// clean up DispatcherListeners
if (!dispatcherListeners.isEmpty()) {
for (DispatcherListener l : dispatcherListeners) {
l.dispatcherDestroyed(this);
}
// WW-5537: Clear the static listener list to release references that may
// pin the webapp classloader after undeploy.
dispatcherListeners.clear();
}
}
/**
* Destroys all interceptors registered in the current configuration.
*
* @since 7.2.0
*/
protected void destroyInterceptors() {
// clean up all interceptors by calling their destroy() method
Set<Interceptor> interceptors = new HashSet<>();
Collection<PackageConfig> packageConfigs = configurationManager.getConfiguration().getPackageConfigs().values();
for (PackageConfig packageConfig : packageConfigs) {
for (Object config : packageConfig.getAllInterceptorConfigs().values()) {
if (config instanceof InterceptorStackConfig isc) {
for (InterceptorMapping interceptorMapping : isc.getInterceptors()) {
if (config instanceof InterceptorStackConfig) {
for (InterceptorMapping interceptorMapping : ((InterceptorStackConfig) config).getInterceptors()) {
interceptors.add(interceptorMapping.getInterceptor());
}
}
@@ -521,38 +480,16 @@ public class Dispatcher {
for (Interceptor interceptor : interceptors) {
interceptor.destroy();
}
}
/**
* Discovers and invokes all {@link InternalDestroyable} beans registered
* in the container, clearing static caches and stopping daemon threads
* to prevent classloader leaks during hot redeployment (WW-5537).
*
* <p>Beans implementing {@link ContextAwareDestroyable} receive the
* {@link jakarta.servlet.ServletContext} via
* {@link ContextAwareDestroyable#destroy(jakarta.servlet.ServletContext)}.</p>
*
* @since 7.2.0
*/
protected void destroyInternalBeans() {
if (configurationManager != null && configurationManager.getConfiguration() != null) {
Container container = configurationManager.getConfiguration().getContainer();
Set<String> destroyableNames = container.getInstanceNames(InternalDestroyable.class);
for (String name : destroyableNames) {
try {
InternalDestroyable destroyable = container.getInstance(InternalDestroyable.class, name);
if (destroyable instanceof ContextAwareDestroyable cad) {
cad.destroy(servletContext);
} else {
destroyable.destroy();
}
} catch (Exception e) {
LOG.warn("Error during internal cleanup [{}]", name, e);
}
}
} else {
LOG.warn("ConfigurationManager is null during cleanup, InternalDestroyable beans will not be invoked");
}
// Clear container holder when application is unloaded / server shutdown
ContainerHolder.clear();
//cleanup action context
ActionContext.clear();
// clean up configuration
configurationManager.destroyConfiguration();
configurationManager = null;
}
private void init_FileManager() throws ClassNotFoundException {
@@ -1,35 +0,0 @@
/*
* 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.struts2.dispatcher;
import org.apache.struts2.inject.util.FinalizableReferenceQueue;
/**
* Adapter that exposes {@link FinalizableReferenceQueue#stopAndClear()} as an
* {@link InternalDestroyable} bean.
*
* @since 7.2.0
*/
public class FinalizableReferenceQueueDestroyable implements InternalDestroyable {
@Override
public void destroy() {
FinalizableReferenceQueue.stopAndClear();
}
}
@@ -1,56 +0,0 @@
/*
* 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.struts2.dispatcher;
import freemarker.ext.beans.BeansWrapper;
import freemarker.template.Configuration;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.apache.struts2.views.freemarker.FreemarkerManager;
import jakarta.servlet.ServletContext;
/**
* WW-5537: Clears FreeMarker's template and class introspection caches
* stored in {@link ServletContext} during application undeploy, preventing
* classloader leaks.
*
* @since 7.2.0
*/
public class FreemarkerCacheDestroyable implements ContextAwareDestroyable {
private static final Logger LOG = LogManager.getLogger(FreemarkerCacheDestroyable.class);
@Override
public void destroy(ServletContext servletContext) {
if (servletContext == null) {
return;
}
Object fmConfig = servletContext.getAttribute(FreemarkerManager.CONFIG_SERVLET_CONTEXT_KEY);
if (fmConfig instanceof Configuration cfg) {
cfg.clearTemplateCache();
cfg.clearEncodingMap();
if (cfg.getObjectWrapper() instanceof BeansWrapper bw) {
bw.clearClassIntrospectionCache();
}
servletContext.removeAttribute(FreemarkerManager.CONFIG_SERVLET_CONTEXT_KEY);
LOG.debug("FreeMarker configuration cleaned up");
}
}
}
@@ -1,45 +0,0 @@
/*
* 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.struts2.dispatcher;
/**
* Internal framework interface for components that hold static state
* (caches, daemon threads, etc.) requiring cleanup during application
* undeploy to prevent classloader leaks.
*
* &lt;p&gt;Implementations are registered as named beans in {@code struts-beans.xml}
* (or plugin descriptors) with type {@code InternalDestroyable}. During
* {@link Dispatcher#cleanup()}, all registered implementations are discovered
* via {@code Container.getInstanceNames(InternalDestroyable.class)} and
* invoked automatically.&lt;/p&gt;
*
* &lt;p&gt;This is not part of the public user API. For user/plugin lifecycle
* callbacks, use {@link DispatcherListener} instead.&lt;/p&gt;
*
* @since 7.2.0
* @see Dispatcher#cleanup()
*/
public interface InternalDestroyable {
/**
* Releases static state held by this component. Called once during
* {@link Dispatcher#cleanup()}.
*/
void destroy();
}
@@ -1,38 +0,0 @@
/*
* 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.struts2.dispatcher;
import org.apache.struts2.ognl.OgnlUtil;
import java.beans.Introspector;
/**
* Clears OGNL runtime caches and JDK introspection caches that hold
* {@code Class<?>} references, preventing classloader leaks on hot redeploy.
*
* @since 7.2.0
*/
public class OgnlCacheDestroyable implements InternalDestroyable {
@Override
public void destroy() {
OgnlUtil.clearRuntimeCache();
Introspector.flushCaches();
}
}
@@ -77,7 +77,6 @@ public class PrepareOperations {
} finally {
ActionContext.clear();
Dispatcher.clearInstance();
ContainerHolder.clear();
devModeOverride.remove();
}
});
@@ -1,35 +0,0 @@
/*
* 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.struts2.dispatcher;
import org.apache.struts2.interceptor.ScopeInterceptor;
/**
* Clears {@link ScopeInterceptor}'s static locks map to prevent classloader
* leaks on hot redeploy.
*
* @since 7.2.0
*/
public class ScopeInterceptorCacheDestroyable implements InternalDestroyable {
@Override
public void destroy() {
ScopeInterceptor.clearLocks();
}
}
@@ -19,7 +19,6 @@
package org.apache.struts2.dispatcher.multipart;
import jakarta.servlet.http.HttpServletRequest;
import org.apache.commons.fileupload2.core.AbstractFileUpload;
import org.apache.commons.fileupload2.core.DiskFileItemFactory;
import org.apache.commons.fileupload2.core.FileUploadByteCountLimitException;
import org.apache.commons.fileupload2.core.FileUploadContentTypeException;
@@ -33,7 +32,6 @@ import org.apache.commons.lang3.StringUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.apache.struts2.StrutsConstants;
import org.apache.struts2.StrutsException;
import org.apache.struts2.dispatcher.LocalizedMessage;
import org.apache.struts2.inject.Inject;
@@ -62,12 +60,6 @@ public abstract class AbstractMultiPartRequest implements MultiPartRequest {
private static final Logger LOG = LogManager.getLogger(AbstractMultiPartRequest.class);
/**
* Verified once per JVM: whether the commons-fileupload2 API on the classpath matches what
* Struts compiled against. Guards against a mismatched milestone resolving at runtime.
*/
private static volatile boolean fileUploadApiVerified;
/**
* Defines the internal buffer size used during streaming operations.
*/
@@ -219,66 +211,23 @@ public abstract class AbstractMultiPartRequest implements MultiPartRequest {
}
protected JakartaServletDiskFileUpload prepareServletFileUpload(Charset charset, Path saveDir) {
ensureFileUploadApiVerified();
JakartaServletDiskFileUpload servletFileUpload = createJakartaFileUpload(charset, saveDir);
if (maxSize != null) {
LOG.debug("Applies max size: {} to file upload request", maxSize);
servletFileUpload.setMaxSize(maxSize);
servletFileUpload.setSizeMax(maxSize);
}
if (maxFiles != null) {
LOG.debug("Applies max files number: {} to file upload request", maxFiles);
servletFileUpload.setMaxFileCount(maxFiles);
servletFileUpload.setFileCountMax(maxFiles);
}
if (maxFileSize != null) {
LOG.debug("Applies max size of single file: {} to file upload request", maxFileSize);
servletFileUpload.setMaxFileSize(maxFileSize);
servletFileUpload.setFileSizeMax(maxFileSize);
}
return servletFileUpload;
}
/**
* Verifies once per JVM that the commons-fileupload2 API on the classpath matches what Struts
* compiled against, failing fast with an actionable message instead of a deep-stack
* {@link NoSuchMethodError} when a mismatched milestone is resolved.
*/
private static void ensureFileUploadApiVerified() {
if (!fileUploadApiVerified) {
verifyFileUploadApi(JakartaServletDiskFileUpload.class);
fileUploadApiVerified = true;
}
}
/**
* Probes {@code uploadClass} for the size-limit setters Struts invokes in
* {@link #prepareServletFileUpload}. Package-private for testing.
*
* @param uploadClass the file upload class to verify
* @throws StrutsException if any required method is absent, indicating a binary-incompatible
* commons-fileupload2 version on the classpath
*/
static void verifyFileUploadApi(Class<?> uploadClass) {
for (String method : new String[]{"setMaxSize", "setMaxFileCount", "setMaxFileSize"}) {
try {
uploadClass.getMethod(method, long.class);
} catch (NoSuchMethodException e) {
throw new StrutsException(String.format(
"Incompatible Apache Commons FileUpload on the classpath: %s.%s(long) is missing. " +
"Detected commons-fileupload2-core version [%s] and commons-fileupload2-jakarta-servlet6 version [%s]. " +
"Align commons-fileupload2-core with commons-fileupload2-jakarta-servlet6 (use the same release for both).",
uploadClass.getName(), method,
implementationVersion(AbstractFileUpload.class),
implementationVersion(uploadClass)), e);
}
}
}
private static String implementationVersion(Class<?> clazz) {
Package pkg = clazz.getPackage();
String version = pkg != null ? pkg.getImplementationVersion() : null;
return version != null ? version : "unknown";
}
protected RequestContext createRequestContext(HttpServletRequest request) {
return new StrutsRequestContext(request);
}
@@ -292,7 +241,9 @@ public abstract class AbstractMultiPartRequest implements MultiPartRequest {
LocalizedMessage localizedMessage = new LocalizedMessage(this.getClass(),
STRUTS_MESSAGES_UPLOAD_ERROR_PARAMETER_TOO_LONG_KEY, null,
new Object[]{fieldName, maxStringLength, fieldValue.length()});
addErrorIfAbsent(localizedMessage);
if (!errors.contains(localizedMessage)) {
errors.add(localizedMessage);
}
return true;
}
return false;
@@ -327,17 +278,15 @@ public abstract class AbstractMultiPartRequest implements MultiPartRequest {
}
LocalizedMessage errorMessage = buildErrorMessage(exClass, e.getMessage(), args);
addErrorIfAbsent(errorMessage);
if (!errors.contains(errorMessage)) {
errors.add(errorMessage);
}
} catch (IOException e) {
LOG.warn("Unable to parse request", e);
LocalizedMessage errorMessage = buildErrorMessage(e.getClass(), e.getMessage(), new Object[]{});
addErrorIfAbsent(errorMessage);
}
}
private void addErrorIfAbsent(LocalizedMessage errorMessage) {
if (!errors.contains(errorMessage)) {
errors.add(errorMessage);
if (!errors.contains(errorMessage)) {
errors.add(errorMessage);
}
}
}
@@ -500,7 +449,9 @@ public abstract class AbstractMultiPartRequest implements MultiPartRequest {
"Empty files are not allowed",
new Object[]{fileName, fieldName}
);
addErrorIfAbsent(errorMessage);
if (!errors.contains(errorMessage)) {
errors.add(errorMessage);
}
return true;
}
return false;
@@ -68,10 +68,11 @@ public class DefaultInterceptorFactory implements InterceptorFactory {
throw new ConfigurationException("Class [" + interceptorClassName + "] does not implement Interceptor", interceptorConfig);
}
reflectionProvider.setProperties(params, interceptor);
if (interceptor instanceof WithLazyParams) {
LOG.debug("Interceptor {} implements {} - expression parameters will be re-evaluated during action invocation",
LOG.debug("Interceptor {} is marked with interface {} and params will be set during action invocation",
interceptorClassName, WithLazyParams.class.getName());
} else {
reflectionProvider.setProperties(params, interceptor);
}
interceptor.init();
@@ -130,13 +130,4 @@ public interface Container extends Serializable {
* Removes the scope strategy for the current thread.
*/
void removeScopeStrategy();
/**
* Releases all internal resources held by this container, including caches,
* factory maps, and thread-local state. This allows the webapp classloader
* to be garbage collected after hot redeployment.
*
* @since 7.2.0
*/
void destroy();
}
@@ -651,25 +651,6 @@ class ContainerImpl implements Container {
void inject(InternalContext context, Object o);
}
/**
* Clears all internal caches, factory maps, and ThreadLocals to release
* Class references that would otherwise pin the webapp classloader after undeploy.
* <p>
* The {@code injectors} and {@code constructors} ReferenceCache maps hold
* {@code Class<?>} keys and reflection accessor objects ({@code Method},
* {@code Constructor}) whose JDK-generated {@code DelegatingClassLoader}
* instances retain the webapp classloader as their parent.
*
* @since 7.2.0
*/
@Override
public void destroy() {
injectors.clear();
constructors.clear();
localContext.remove();
localScopeStrategy.remove();
}
static class MissingDependencyException extends Exception {
MissingDependencyException(String message) {
super(message);
@@ -18,7 +18,6 @@ package org.apache.struts2.inject.util;
import java.lang.ref.Reference;
import java.lang.ref.ReferenceQueue;
import java.util.concurrent.atomic.AtomicReference;
import java.util.logging.Level;
import java.util.logging.Logger;
@@ -27,13 +26,11 @@ import java.util.logging.Logger;
*
* @author Bob Lee (crazybob@google.com)
*/
public class FinalizableReferenceQueue extends ReferenceQueue<Object> {
class FinalizableReferenceQueue extends ReferenceQueue<Object> {
private static final Logger logger =
Logger.getLogger(FinalizableReferenceQueue.class.getName());
private final AtomicReference<Thread> cleanupThread = new AtomicReference<>();
private FinalizableReferenceQueue() {}
void cleanUp(Reference reference) {
@@ -52,39 +49,18 @@ public class FinalizableReferenceQueue extends ReferenceQueue<Object> {
Thread thread = new Thread("FinalizableReferenceQueue") {
@Override
public void run() {
while (!Thread.currentThread().isInterrupted()) {
while (true) {
try {
cleanUp(remove());
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
break;
}
} catch (InterruptedException e) { /* ignore */ }
}
}
};
thread.setDaemon(true);
thread.start();
cleanupThread.set(thread);
}
/**
* Stops the background cleanup thread to prevent classloader memory leaks during hot redeployment.
*/
void stop() {
Thread t = cleanupThread.getAndSet(null);
if (t != null) {
t.interrupt();
try {
t.join(5000);
} catch (InterruptedException ignored) {
Thread.currentThread().interrupt();
}
t.setContextClassLoader(null);
}
}
private static final AtomicReference<ReferenceQueue<Object>> instance =
new AtomicReference<>(createAndStart());
static ReferenceQueue<Object> instance = createAndStart();
static FinalizableReferenceQueue createAndStart() {
FinalizableReferenceQueue queue = new FinalizableReferenceQueue();
@@ -96,17 +72,6 @@ public class FinalizableReferenceQueue extends ReferenceQueue<Object> {
* Gets instance.
*/
public static ReferenceQueue<Object> getInstance() {
return instance.get();
}
/**
* Stops the cleanup thread and clears the instance to prevent classloader
* memory leaks during hot redeployment.
*/
public static void stopAndClear() {
ReferenceQueue<Object> q = instance.getAndSet(null);
if (q instanceof FinalizableReferenceQueue frq) {
frq.stop();
}
return instance;
}
}
@@ -110,8 +110,8 @@ public abstract class AbstractFileUploadInterceptor extends AbstractInterceptor
Set<String> errorMessages = new HashSet<>();
ValidationAware validation = null;
if (action instanceof ValidationAware validationAware) {
validation = validationAware;
if (action instanceof ValidationAware) {
validation = (ValidationAware) action;
}
// If it's null the upload failed
@@ -125,7 +125,7 @@ public abstract class AbstractFileUploadInterceptor extends AbstractInterceptor
}
if (file.getContent() == null) {
String errMsg = getTextMessage(action, STRUTS_MESSAGES_INVALID_CONTENT_TYPE_KEY, new String[]{originalFilename});
String errMsg = getTextMessage(action, STRUTS_MESSAGES_ERROR_UPLOADING_KEY, new String[]{originalFilename});
errorMessages.add(errMsg);
LOG.warn(errMsg);
}
@@ -200,13 +200,24 @@ public abstract class AbstractFileUploadInterceptor extends AbstractInterceptor
return matcher.match(new HashMap<>(), text, o);
}
protected boolean isNonEmpty(Object[] objArray) {
boolean result = false;
for (Object o : objArray) {
if (o != null) {
result = true;
break;
}
}
return result;
}
protected String getTextMessage(String messageKey, String[] args) {
return getTextMessage(this, messageKey, args);
}
protected String getTextMessage(Object action, String messageKey, String[] args) {
if (action instanceof TextProvider textProvider) {
return textProvider.getText(messageKey, args);
if (action instanceof TextProvider) {
return ((TextProvider) action).getText(messageKey, args);
}
return getTextProvider(action).getText(messageKey, args);
}
@@ -218,8 +229,8 @@ public abstract class AbstractFileUploadInterceptor extends AbstractInterceptor
private LocaleProvider getLocaleProvider(Object action) {
LocaleProvider localeProvider;
if (action instanceof LocaleProvider lp) {
localeProvider = lp;
if (action instanceof LocaleProvider) {
localeProvider = (LocaleProvider) action;
} else {
LocaleProviderFactory localeProviderFactory = container.getInstance(LocaleProviderFactory.class);
localeProvider = localeProviderFactory.createLocaleProvider();
@@ -229,8 +240,8 @@ public abstract class AbstractFileUploadInterceptor extends AbstractInterceptor
protected void applyValidation(Object action, MultiPartRequestWrapper multiWrapper) {
ValidationAware validation = null;
if (action instanceof ValidationAware va) {
validation = va;
if (action instanceof ValidationAware) {
validation = (ValidationAware) action;
}
if (multiWrapper.hasErrors() && validation != null) {
@@ -71,79 +71,6 @@ import java.util.List;
* a file reference to be set on the action. If none is specified allow all extensions to be uploaded.</li>
* </ul>
*
* <h3>Dynamic Parameter Evaluation</h3>
* <p>
* This interceptor implements {@link WithLazyParams}, which enables dynamic parameter evaluation at runtime.
* Parameters can use <code>${...}</code> expressions that will be evaluated against the ValueStack for each request,
* allowing file upload validation rules to be determined dynamically based on action properties, session data,
* or other runtime values.
* </p>
*
* <p><strong>Static configuration example:</strong></p>
* <pre>
* &lt;action name="upload" class="com.example.UploadAction"&gt;
* &lt;interceptor-ref name="actionFileUpload"&gt;
* &lt;param name="allowedTypes"&gt;image/jpeg,image/png,application/pdf&lt;/param&gt;
* &lt;param name="allowedExtensions"&gt;.jpg,.png,.pdf&lt;/param&gt;
* &lt;param name="maximumSize"&gt;5242880&lt;/param&gt;
* &lt;/interceptor-ref&gt;
* &lt;interceptor-ref name="basicStack"/&gt;
* &lt;/action&gt;
* </pre>
*
* <p><strong>Dynamic configuration example:</strong></p>
* <pre>
* &lt;action name="dynamicUpload" class="com.example.DynamicUploadAction"&gt;
* &lt;interceptor-ref name="actionFileUpload"&gt;
* &lt;param name="allowedTypes"&gt;${uploadConfig.allowedMimeTypes}&lt;/param&gt;
* &lt;param name="allowedExtensions"&gt;${uploadConfig.allowedExtensions}&lt;/param&gt;
* &lt;param name="maximumSize"&gt;${uploadConfig.maxFileSize}&lt;/param&gt;
* &lt;/interceptor-ref&gt;
* &lt;interceptor-ref name="basicStack"/&gt;
* &lt;/action&gt;
* </pre>
*
* <p><strong>Action class with dynamic configuration:</strong></p>
* <pre>
* package com.example;
*
* import org.apache.struts2.ActionSupport;
* import org.apache.struts2.action.UploadedFilesAware;
*
* public class DynamicUploadAction extends ActionSupport implements UploadedFilesAware {
* private UploadedFile uploadedFile;
* private UploadConfig uploadConfig;
*
* public void prepare() {
* // Load configuration dynamically (from database, properties, etc.)
* uploadConfig = new UploadConfig();
* uploadConfig.setAllowedMimeTypes("image/jpeg,image/png");
* uploadConfig.setAllowedExtensions(".jpg,.png");
* uploadConfig.setMaxFileSize(5242880L);
* }
*
* &#064;Override
* public void withUploadedFiles(List&lt;UploadedFile&gt; uploadedFiles) {
* if (!uploadedFiles.isEmpty()) {
* this.uploadedFile = uploadedFiles.get(0);
* }
* }
*
* public UploadConfig getUploadConfig() {
* return uploadConfig;
* }
*
* public String execute() {
* //...
* return SUCCESS;
* }
* }
* </pre>
*
* <p><strong>Performance Note:</strong> When using dynamic parameters with <code>${...}</code> expressions,
* parameters are evaluated for each request. For static validation rules, use literal values for better performance.
* </p>
*
* <p>Example code:</p>
*
* <pre>
@@ -197,12 +124,8 @@ import java.util.List;
* }
* }
* </pre>
*
* @see WithLazyParams
* @see UploadedFilesAware
* @see AbstractFileUploadInterceptor
*/
public class ActionFileUploadInterceptor extends AbstractFileUploadInterceptor implements WithLazyParams {
public class ActionFileUploadInterceptor extends AbstractFileUploadInterceptor {
protected static final Logger LOG = LogManager.getLogger(ActionFileUploadInterceptor.class);
@@ -18,33 +18,26 @@
*/
package org.apache.struts2.interceptor;
import org.apache.commons.lang3.BooleanUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.apache.struts2.ActionInvocation;
import org.apache.struts2.StrutsConstants;
import org.apache.struts2.Unchainable;
import org.apache.struts2.inject.Inject;
import org.apache.struts2.interceptor.parameter.ParameterAuthorizer;
import org.apache.struts2.ognl.OgnlUtil;
import org.apache.struts2.result.ActionChainResult;
import org.apache.struts2.result.Result;
import org.apache.struts2.util.CompoundRoot;
import org.apache.struts2.util.ProxyService;
import org.apache.struts2.util.ProxyUtil;
import org.apache.struts2.util.TextParseUtil;
import org.apache.struts2.util.ValueStack;
import org.apache.struts2.util.reflection.ReflectionProvider;
import java.beans.BeanInfo;
import java.beans.IntrospectionException;
import java.beans.PropertyDescriptor;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
@@ -75,9 +68,6 @@ import java.util.Set;
* <li>struts.chaining.copyErrors - set to true to copy Action Errors</li>
* <li>struts.chaining.copyFieldErrors - set to true to copy Field Errors</li>
* <li>struts.chaining.copyMessages - set to true to copy Action Messages</li>
* <li>struts.chaining.requireAnnotations - set to true to only copy properties whose target
* Action member is annotated with {@code @StrutsParameter} (opt-in, default false). When the
* target cannot be introspected, no properties are copied (fail closed).</li>
* </ul>
*
* <p>
@@ -106,7 +96,7 @@ import java.util.Set;
* </p>
* <!-- END SNIPPET: extending -->
* <u>Example code:</u>
* <p>
*
* <!-- START SNIPPET: example -->
* <pre>
* &lt;action name="someAction" class="com.examples.SomeAction"&gt;
@@ -124,6 +114,7 @@ import java.util.Set;
* </pre>
* <!-- END SNIPPET: example -->
*
*
* @author mrdon
* @author tm_jee ( tm_jee(at)yahoo.co.uk )
* @see ActionChainResult
@@ -144,36 +135,12 @@ public class ChainingInterceptor extends AbstractInterceptor {
protected Collection<String> includes;
protected ReflectionProvider reflectionProvider;
private ProxyService proxyService;
private boolean requireAnnotations = false;
private transient ParameterAuthorizer parameterAuthorizer;
private transient OgnlUtil ognlUtil;
@Inject
public void setReflectionProvider(ReflectionProvider prov) {
this.reflectionProvider = prov;
}
@Inject
public void setProxyService(ProxyService proxyService) {
this.proxyService = proxyService;
}
@Inject
public void setParameterAuthorizer(ParameterAuthorizer parameterAuthorizer) {
this.parameterAuthorizer = parameterAuthorizer;
}
@Inject
public void setOgnlUtil(OgnlUtil ognlUtil) {
this.ognlUtil = ognlUtil;
}
@Inject(value = StrutsConstants.STRUTS_CHAINING_REQUIRE_ANNOTATIONS, required = false)
public void setRequireAnnotations(String requireAnnotations) {
this.requireAnnotations = BooleanUtils.toBoolean(requireAnnotations);
}
@Inject(value = StrutsConstants.STRUTS_CHAINING_COPY_ERRORS, required = false)
public void setCopyErrors(String copyErrors) {
this.copyErrors = "true".equalsIgnoreCase(copyErrors);
@@ -203,70 +170,21 @@ public class ChainingInterceptor extends AbstractInterceptor {
List<Object> list = prepareList(root);
Map<String, Object> ctxMap = invocation.getInvocationContext().getContextMap();
for (Object object : list) {
if (shouldCopy(object)) {
copyObjectToAction(object, invocation.getAction(), ctxMap);
}
}
}
private void copyObjectToAction(Object object, Object action, Map<String, Object> ctxMap) {
Class<?> editable = null;
if (proxyService.isProxy(action)) {
editable = proxyService.ultimateTargetClass(action);
}
Collection<String> copyExcludes = prepareExcludes();
if (requireAnnotations) {
Class<?> targetClass = editable != null ? editable : action.getClass();
BeanInfo beanInfo = getTargetBeanInfo(targetClass);
if (beanInfo == null) {
// Fail closed: cannot prove which properties are annotated, so copy nothing.
LOG.warn("Chaining: unable to introspect target [{}]; skipping property copy " +
"(struts.chaining.requireAnnotations enabled)", targetClass.getName());
return;
}
copyExcludes = excludeUnauthorizedProperties(copyExcludes, beanInfo, targetClass, action);
}
reflectionProvider.copy(object, action, ctxMap, copyExcludes, includes, editable);
}
/**
* Returns the excludes to use for the copy: the base excludes unioned with the names of all
* writable target properties that are not authorized by {@code @StrutsParameter}.
*/
private Collection<String> excludeUnauthorizedProperties(Collection<String> baseExcludes,
BeanInfo beanInfo, Class<?> targetClass, Object action) {
Set<String> merged = new HashSet<>();
if (baseExcludes != null) {
merged.addAll(baseExcludes);
}
for (PropertyDescriptor descriptor : beanInfo.getPropertyDescriptors()) {
if (descriptor.getWriteMethod() == null) {
if (!shouldCopy(object)) {
continue;
}
String name = descriptor.getName();
// target == action is deliberate: chaining copies onto the action object itself (not a
// ModelDriven model), so the authorizer's ModelDriven exemption must not apply here.
if (!parameterAuthorizer.isAuthorized(name, action, action)) {
LOG.warn("Chaining: property [{}] not copied to [{}] because it is not annotated with @StrutsParameter",
name, targetClass.getName());
merged.add(name);
Object action = invocation.getAction();
Class<?> editable = null;
if (ProxyUtil.isProxy(action)) {
editable = ProxyUtil.ultimateTargetClass(action);
}
}
return merged;
}
private BeanInfo getTargetBeanInfo(Class<?> targetClass) {
try {
return ognlUtil.getBeanInfo(targetClass);
} catch (IntrospectionException e) {
LOG.warn("Chaining: error introspecting target [{}] for @StrutsParameter enforcement", targetClass, e);
return null;
reflectionProvider.copy(object, action, ctxMap, prepareExcludes(), includes, editable);
}
}
private Collection<String> prepareExcludes() {
Collection<String> localExcludes = excludes;
if (!copyErrors || !copyMessages || !copyFieldErrors) {
if (!copyErrors || !copyMessages ||!copyFieldErrors) {
if (localExcludes == null) {
localExcludes = new HashSet<>();
if (!copyErrors) {
@@ -19,8 +19,6 @@
package org.apache.struts2.interceptor;
import org.apache.struts2.ActionInvocation;
import org.apache.struts2.StrutsConstants;
import org.apache.struts2.inject.Inject;
import org.apache.struts2.interceptor.AbstractInterceptor;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
@@ -41,27 +39,24 @@ import java.util.Set;
* of 'false'.
* </p>
* <!-- END SNIPPET: description -->
* <p>
*
* <!-- START SNIPPET: parameters -->
* <ul>
* <li>setUncheckedValue - The default value of an unchecked box can be overridden by setting the 'uncheckedValue' property.</li>
* </ul>
* <!-- END SNIPPET: parameters -->
* <p>
*
* <!-- START SNIPPET: extending -->
* <p>
*
* <!-- END SNIPPET: extending -->
*/
public class CheckboxInterceptor extends AbstractInterceptor {
/**
* Auto-generated serialization id
*/
/** Auto-generated serialization id */
@Serial
private static final long serialVersionUID = -586878104807229585L;
private String uncheckedValue = Boolean.FALSE.toString();
private String hiddenPrefix = "__checkbox_";
private static final Logger LOG = LogManager.getLogger(CheckboxInterceptor.class);
@@ -73,8 +68,8 @@ public class CheckboxInterceptor extends AbstractInterceptor {
Set<String> checkboxParameters = new HashSet<>();
for (Map.Entry<String, Parameter> parameter : parameters.entrySet()) {
String name = parameter.getKey();
if (name.startsWith(hiddenPrefix)) {
String checkboxName = name.substring(hiddenPrefix.length());
if (name.startsWith("__checkbox_")) {
String checkboxName = name.substring("__checkbox_".length());
Parameter value = parameter.getValue();
checkboxParameters.add(name);
@@ -105,16 +100,4 @@ public class CheckboxInterceptor extends AbstractInterceptor {
public void setUncheckedValue(String uncheckedValue) {
this.uncheckedValue = uncheckedValue;
}
/**
* Sets the prefix used for hidden checkbox fields.
* Default is "__checkbox_" for backward compatibility.
*
* @param hiddenPrefix The prefix to use for hidden checkbox fields
* @since 7.2.0
*/
@Inject(value = StrutsConstants.STRUTS_UI_CHECKBOX_HIDDEN_PREFIX, required = false)
public void setHiddenPrefix(String hiddenPrefix) {
this.hiddenPrefix = hiddenPrefix;
}
}
@@ -26,8 +26,6 @@ import org.apache.struts2.ActionInvocation;
import org.apache.struts2.ServletActionContext;
import org.apache.struts2.action.CookiesAware;
import org.apache.struts2.inject.Inject;
import org.apache.struts2.interceptor.parameter.ParameterAllowlister;
import org.apache.struts2.interceptor.parameter.ParameterAuthorizer;
import org.apache.struts2.security.AcceptedPatternsChecker;
import org.apache.struts2.security.ExcludedPatternsChecker;
import org.apache.struts2.util.TextParseUtil;
@@ -101,16 +99,8 @@ import java.util.Set;
*
* <ul>
* <li>
* populateCookieValueIntoStack(name, value, map, stack, action) - the preferred extension point
* since 7.2.0. The default implementation gates the cookie write through
* {@link org.apache.struts2.interceptor.parameter.ParameterAuthorizer} and primes the OGNL allowlist via
* {@link org.apache.struts2.interceptor.parameter.ParameterAllowlister} before delegating to the legacy
* 4-arg {@code populateCookieValueIntoStack}. Override here to customize the authorization behavior itself.
* </li>
* <li>
* populateCookieValueIntoStack(name, value, map, stack) - <em>deprecated since 7.2.0</em>. The legacy
* hook that performs the actual {@code stack.setValue}. Existing overrides continue to work and
* automatically receive only authorized cookies via the 5-arg default.
* populateCookieValueIntoStack - this method will decide if this cookie value is qualified
* to be populated into the value stack (hence into the action itself)
* </li>
* <li>
* injectIntoCookiesAwareAction - this method will inject selected cookies (as a java.util.Map)
@@ -197,8 +187,6 @@ public class CookieInterceptor extends AbstractInterceptor {
private ExcludedPatternsChecker excludedPatternsChecker;
private AcceptedPatternsChecker acceptedPatternsChecker;
private transient ParameterAuthorizer parameterAuthorizer;
private transient ParameterAllowlister parameterAllowlister;
@Inject
public void setExcludedPatternsChecker(ExcludedPatternsChecker excludedPatternsChecker) {
@@ -211,16 +199,6 @@ public class CookieInterceptor extends AbstractInterceptor {
this.acceptedPatternsChecker.setAcceptedPatterns(ACCEPTED_PATTERN);
}
@Inject
public void setParameterAuthorizer(ParameterAuthorizer parameterAuthorizer) {
this.parameterAuthorizer = parameterAuthorizer;
}
@Inject
public void setParameterAllowlister(ParameterAllowlister parameterAllowlister) {
this.parameterAllowlister = parameterAllowlister;
}
/**
* @param cookiesName the <code>cookiesName</code> which if matched will allow the cookie
* to be injected into action, could be comma-separated string.
@@ -256,8 +234,6 @@ public class CookieInterceptor extends AbstractInterceptor {
public String intercept(ActionInvocation invocation) throws Exception {
LOG.debug("start interception");
final Object action = invocation.getAction();
// contains selected cookies
final Map<String, String> cookiesMap = new LinkedHashMap<>();
@@ -272,9 +248,9 @@ public class CookieInterceptor extends AbstractInterceptor {
if (isAcceptableName(name)) {
if (cookiesNameSet.contains("*")) {
LOG.debug("Contains cookie name [*] in configured cookies name set, cookie with name [{}] with value [{}] will be injected", name, value);
populateCookieValueIntoStack(name, value, cookiesMap, stack, action);
populateCookieValueIntoStack(name, value, cookiesMap, stack);
} else if (cookiesNameSet.contains(cookie.getName())) {
populateCookieValueIntoStack(name, value, cookiesMap, stack, action);
populateCookieValueIntoStack(name, value, cookiesMap, stack);
}
} else {
LOG.warn("Cookie name [{}] with value [{}] was rejected!", name, value);
@@ -283,7 +259,7 @@ public class CookieInterceptor extends AbstractInterceptor {
}
// inject the cookiesMap, even if we don't have any cookies
injectIntoCookiesAwareAction(action, cookiesMap);
injectIntoCookiesAwareAction(invocation.getAction(), cookiesMap);
return invocation.invoke();
}
@@ -338,30 +314,6 @@ public class CookieInterceptor extends AbstractInterceptor {
return false;
}
/**
* Authorizes the cookie against {@link ParameterAuthorizer}, primes OGNL allowlist for any nested path via
* {@link ParameterAllowlister}, then delegates to the legacy {@link #populateCookieValueIntoStack(String, String,
* Map, ValueStack)} hook so existing subclass overrides continue to participate. Override this method to customize
* the authorization behavior itself.
*
* @param cookieName cookie name (potentially an OGNL path; {@code ACCEPTED_PATTERN} restricts the character set)
* @param cookieValue cookie value
* @param cookiesMap map of cookies populated for {@link org.apache.struts2.action.CookiesAware}
* @param stack current request value stack
* @param action the action instance from {@link ActionInvocation#getAction()}; used for {@code @StrutsParameter} target resolution
* @since 7.2.0
*/
@SuppressWarnings("deprecation") // intentional: delegating to the deprecated 4-arg form is the contract that lets existing subclass overrides participate
protected void populateCookieValueIntoStack(String cookieName, String cookieValue, Map<String, String> cookiesMap, ValueStack stack, Object action) {
Object target = parameterAuthorizer.resolveTarget(action);
if (!parameterAuthorizer.isAuthorized(cookieName, target, action)) {
LOG.debug("Cookie [{}] rejected by @StrutsParameter authorization on target [{}]", cookieName, target.getClass().getSimpleName());
return;
}
parameterAllowlister.primeAllowlistForPath(cookieName, target);
populateCookieValueIntoStack(cookieName, cookieValue, cookiesMap, stack);
}
/**
* Hook that populate cookie value into value stack (hence the action)
* if the criteria is satisfied (if the cookie value matches with those configured).
@@ -370,12 +322,7 @@ public class CookieInterceptor extends AbstractInterceptor {
* @param cookieValue cookie value
* @param cookiesMap map of cookies
* @param stack value stack
* @deprecated since 7.2.0. Override
* {@link #populateCookieValueIntoStack(String, String, Map, ValueStack, Object)} instead so cookie writes are
* authorized by {@link ParameterAuthorizer}. The default 5-arg implementation calls this method after the
* authorization gate, so existing overrides continue to receive only authorized cookies.
*/
@Deprecated(since = "7.2.0")
protected void populateCookieValueIntoStack(String cookieName, String cookieValue, Map<String, String> cookiesMap, ValueStack stack) {
if (cookiesValueSet.isEmpty() || cookiesValueSet.contains("*")) {
// If the interceptor is configured to accept any cookie value
@@ -84,8 +84,8 @@ import java.util.Map;
*
* <pre>
* <!-- START SNIPPET: example -->
* &lt;struts&gt;
* &lt;package name="default" extends="struts-default"&gt;
* &lt;xwork&gt;
* &lt;package name="default" extends="xwork-default"&gt;
* &lt;global-results&gt;
* &lt;result name="error" type="freemarker"&gt;error.ftl&lt;/result&gt;
* &lt;/global-results&gt;
@@ -102,7 +102,7 @@ import java.util.Map;
* &lt;result name="success" type="freemarker"&gt;test.ftl&lt;/result&gt;
* &lt;/action&gt;
* &lt;/package&gt;
* &lt;/struts&gt;
* &lt;/xwork&gt;
* <!-- END SNIPPET: example -->
* </pre>
*
@@ -113,8 +113,8 @@ import java.util.Map;
*
* <pre>
* <!-- START SNIPPET: example2 -->
* &lt;struts&gt;
* &lt;package name="something" extends="struts-default"&gt;
* &lt;xwork&gt;
* &lt;package name="something" extends="xwork-default"&gt;
* &lt;interceptors&gt;
* &lt;interceptor-stack name="exceptionmappingStack"&gt;
* &lt;interceptor-ref name="exception"&gt;
@@ -150,7 +150,7 @@ import java.util.Map;
* &lt;/action&gt;
*
* &lt;/package&gt;
* &lt;/struts&gt;
* &lt;/xwork&gt;
* <!-- END SNIPPET: example2 -->
* </pre>
*
@@ -257,7 +257,7 @@ public class ExceptionMappingInterceptor extends AbstractInterceptor {
}
Level level = Level.getLevel(logLevel);
if (level == null) {
if (level == null) {
throw new IllegalArgumentException("LogLevel [" + logLevel + "] is not supported");
}
logger.log(level, e.getMessage(), e);
@@ -26,11 +26,18 @@ import org.apache.struts2.util.TextParseUtil;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.apache.logging.log4j.message.ParameterizedMessage;
import org.apache.struts2.ServletActionContext;
import org.apache.struts2.dispatcher.HttpParameters;
import org.apache.struts2.dispatcher.Parameter;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession;
import java.util.Collections;
import java.util.Enumeration;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
@@ -101,10 +108,6 @@ public class I18nInterceptor extends AbstractInterceptor {
.collect(Collectors.toSet());
}
protected boolean isLocaleSupported(Locale locale) {
return supportedLocale.isEmpty() || supportedLocale.contains(locale);
}
@Inject
public void setLocaleProviderFactory(LocaleProviderFactory localeProviderFactory) {
this.localeProviderFactory = localeProviderFactory;
@@ -216,167 +219,202 @@ public class I18nInterceptor extends AbstractInterceptor {
/**
* Uses to handle reading/storing Locale from/in different locations
*/
@Deprecated(forRemoval = true, since = "7.2.0")
protected interface LocaleHandler extends org.apache.struts2.interceptor.i18n.LocaleHandler {
protected interface LocaleHandler {
Locale find();
Locale read(ActionInvocation invocation);
Locale store(ActionInvocation invocation, Locale locale);
boolean shouldStore();
}
/**
* @deprecated Since 7.2.0, use the top-level handler classes in {@code org.apache.struts2.interceptor.i18n}.
* Scheduled for removal in the next release cycle.
*/
@Deprecated(forRemoval = true, since = "7.2.0")
protected abstract class LocaleHandlerAdapter implements LocaleHandler {
protected class RequestLocaleHandler implements LocaleHandler {
private final org.apache.struts2.interceptor.i18n.LocaleHandler delegate;
protected ActionInvocation actionInvocation;
protected boolean shouldStore = true;
protected LocaleHandlerAdapter(org.apache.struts2.interceptor.i18n.LocaleHandler delegate) {
this.delegate = delegate;
protected RequestLocaleHandler(ActionInvocation invocation) {
actionInvocation = invocation;
}
@Override
public Locale find() {
return delegate.find();
}
LOG.debug("Searching locale in request under parameter {}", requestOnlyParameterName);
@Override
public Locale read(ActionInvocation invocation) {
return delegate.read(invocation);
Parameter requestedLocale = findLocaleParameter(actionInvocation, requestOnlyParameterName);
if (requestedLocale.isDefined()) {
return getLocaleFromParam(requestedLocale.getValue());
}
return null;
}
@Override
public Locale store(ActionInvocation invocation, Locale locale) {
return delegate.store(invocation, locale);
return locale;
}
@Override
public Locale read(ActionInvocation invocation) {
LOG.debug("Searching current Invocation context");
// no overriding locale definition found, stay with current invocation (=browser) locale
Locale locale = invocation.getInvocationContext().getLocale();
if (locale != null) {
LOG.debug("Applied invocation context locale: {}", locale);
}
return locale;
}
@Override
public boolean shouldStore() {
return delegate.shouldStore();
return shouldStore;
}
}
private org.apache.struts2.interceptor.i18n.RequestLocaleHandler createRequestDelegate(ActionInvocation invocation, String requestOnlyParam) {
return new org.apache.struts2.interceptor.i18n.RequestLocaleHandler(invocation, requestOnlyParam) {
@Override
protected Locale getLocaleFromParam(String requestedLocale) {
return I18nInterceptor.this.getLocaleFromParam(requestedLocale);
}
protected class AcceptLanguageLocaleHandler extends RequestLocaleHandler {
@Override
protected Parameter findLocaleParameter(ActionInvocation inv, String paramName) {
return I18nInterceptor.this.findLocaleParameter(inv, paramName);
}
@Override
protected boolean isLocaleSupported(Locale locale) {
return I18nInterceptor.this.isLocaleSupported(locale);
}
};
}
private org.apache.struts2.interceptor.i18n.AcceptLanguageLocaleHandler createAcceptLanguageDelegate(ActionInvocation invocation) {
return new org.apache.struts2.interceptor.i18n.AcceptLanguageLocaleHandler(
invocation, requestOnlyParameterName, supportedLocale
) {
@Override
protected Locale getLocaleFromParam(String requestedLocale) {
return I18nInterceptor.this.getLocaleFromParam(requestedLocale);
}
@Override
protected Parameter findLocaleParameter(ActionInvocation inv, String paramName) {
return I18nInterceptor.this.findLocaleParameter(inv, paramName);
}
@Override
protected boolean isLocaleSupported(Locale locale) {
return I18nInterceptor.this.isLocaleSupported(locale);
}
};
}
private org.apache.struts2.interceptor.i18n.SessionLocaleHandler createSessionDelegate(ActionInvocation invocation) {
return new org.apache.struts2.interceptor.i18n.SessionLocaleHandler(
invocation, requestOnlyParameterName, supportedLocale, parameterName, attributeName
) {
@Override
protected Locale getLocaleFromParam(String requestedLocale) {
return I18nInterceptor.this.getLocaleFromParam(requestedLocale);
}
@Override
protected Parameter findLocaleParameter(ActionInvocation inv, String paramName) {
return I18nInterceptor.this.findLocaleParameter(inv, paramName);
}
@Override
protected boolean isLocaleSupported(Locale locale) {
return I18nInterceptor.this.isLocaleSupported(locale);
}
};
}
private org.apache.struts2.interceptor.i18n.CookieLocaleHandler createCookieDelegate(ActionInvocation invocation) {
return new org.apache.struts2.interceptor.i18n.CookieLocaleHandler(
invocation, requestOnlyParameterName, supportedLocale, requestCookieParameterName, attributeName
) {
@Override
protected Locale getLocaleFromParam(String requestedLocale) {
return I18nInterceptor.this.getLocaleFromParam(requestedLocale);
}
@Override
protected Parameter findLocaleParameter(ActionInvocation inv, String paramName) {
return I18nInterceptor.this.findLocaleParameter(inv, paramName);
}
@Override
protected boolean isLocaleSupported(Locale locale) {
return I18nInterceptor.this.isLocaleSupported(locale);
}
};
}
/**
* @deprecated Since 7.2.0, use {@link org.apache.struts2.interceptor.i18n.RequestLocaleHandler}.
* Scheduled for removal in the next release cycle.
*/
@Deprecated(forRemoval = true, since = "7.2.0")
protected class RequestLocaleHandler extends LocaleHandlerAdapter {
protected RequestLocaleHandler(ActionInvocation invocation) {
super(createRequestDelegate(invocation, requestOnlyParameterName));
}
}
/**
* @deprecated Since 7.2.0, use {@link org.apache.struts2.interceptor.i18n.AcceptLanguageLocaleHandler}.
* Scheduled for removal in the next release cycle.
*/
@Deprecated(forRemoval = true, since = "7.2.0")
protected class AcceptLanguageLocaleHandler extends LocaleHandlerAdapter {
protected AcceptLanguageLocaleHandler(ActionInvocation invocation) {
super(createAcceptLanguageDelegate(invocation));
super(invocation);
}
@Override
@SuppressWarnings("rawtypes")
public Locale find() {
if (!supportedLocale.isEmpty()) {
Enumeration locales = actionInvocation.getInvocationContext().getServletRequest().getLocales();
while (locales.hasMoreElements()) {
Locale locale = (Locale) locales.nextElement();
if (supportedLocale.contains(locale)) {
return locale;
}
}
}
return super.find();
}
}
/**
* @deprecated Since 7.2.0, use {@link org.apache.struts2.interceptor.i18n.SessionLocaleHandler}.
* Scheduled for removal in the next release cycle.
*/
@Deprecated(forRemoval = true, since = "7.2.0")
protected class SessionLocaleHandler extends LocaleHandlerAdapter {
protected class SessionLocaleHandler extends AcceptLanguageLocaleHandler {
protected SessionLocaleHandler(ActionInvocation invocation) {
super(createSessionDelegate(invocation));
super(invocation);
}
@Override
public Locale find() {
Locale requestOnlyLocale = super.find();
if (requestOnlyLocale != null) {
LOG.debug("Found locale under request only param, it won't be stored in session!");
shouldStore = false;
return requestOnlyLocale;
}
LOG.debug("Searching locale in request under parameter {}", parameterName);
Parameter requestedLocale = findLocaleParameter(actionInvocation, parameterName);
if (requestedLocale.isDefined()) {
return getLocaleFromParam(requestedLocale.getValue());
}
return null;
}
@Override
public Locale store(ActionInvocation invocation, Locale locale) {
Map<String, Object> session = invocation.getInvocationContext().getSession();
if (session != null) {
String sessionId = ServletActionContext.getRequest().getSession().getId();
synchronized (sessionId.intern()) {
session.put(attributeName, locale);
}
}
return locale;
}
@Override
public Locale read(ActionInvocation invocation) {
Locale locale = null;
LOG.debug("Checks session for saved locale");
HttpSession session = ServletActionContext.getRequest().getSession(false);
if (session != null) {
String sessionId = session.getId();
synchronized (sessionId.intern()) {
Object sessionLocale = invocation.getInvocationContext().getSession().get(attributeName);
if (sessionLocale instanceof Locale) {
locale = (Locale) sessionLocale;
LOG.debug("Applied session locale: {}", locale);
}
}
}
if (locale == null) {
LOG.debug("No Locale defined in session, fetching from current request and it won't be stored in session!");
shouldStore = false;
locale = super.read(invocation);
} else {
LOG.debug("Found stored Locale {} in session, using it!", locale);
}
return locale;
}
}
/**
* @deprecated Since 7.2.0, use {@link org.apache.struts2.interceptor.i18n.CookieLocaleHandler}.
* Scheduled for removal in the next release cycle.
*/
@Deprecated(forRemoval = true, since = "7.2.0")
protected class CookieLocaleHandler extends LocaleHandlerAdapter {
protected class CookieLocaleHandler extends AcceptLanguageLocaleHandler {
protected CookieLocaleHandler(ActionInvocation invocation) {
super(createCookieDelegate(invocation));
super(invocation);
}
@Override
public Locale find() {
Locale requestOnlySessionLocale = super.find();
if (requestOnlySessionLocale != null) {
shouldStore = false;
return requestOnlySessionLocale;
}
LOG.debug("Searching locale in request under parameter {}", requestCookieParameterName);
Parameter requestedLocale = findLocaleParameter(actionInvocation, requestCookieParameterName);
if (requestedLocale.isDefined()) {
return getLocaleFromParam(requestedLocale.getValue());
}
return null;
}
@Override
public Locale store(ActionInvocation invocation, Locale locale) {
HttpServletResponse response = ServletActionContext.getResponse();
Cookie cookie = new Cookie(attributeName, locale.toString());
cookie.setMaxAge(1209600); // two weeks
response.addCookie(cookie);
return locale;
}
@Override
public Locale read(ActionInvocation invocation) {
Locale locale = null;
Cookie[] cookies = ServletActionContext.getRequest().getCookies();
if (cookies != null) {
for (Cookie cookie : cookies) {
if (attributeName.equals(cookie.getName())) {
locale = getLocaleFromParam(cookie.getValue());
}
}
}
if (locale == null) {
LOG.debug("No Locale defined in cookie, fetching from current request and it won't be stored!");
shouldStore = false;
locale = super.read(invocation);
} else {
LOG.debug("Found stored Locale {} in cookie, using it!", locale);
}
return locale;
}
}
@@ -263,15 +263,6 @@ public class ScopeInterceptor extends AbstractInterceptor implements PreResultLi
}
}
/**
* Clears the locks map to prevent memory leaks during hot redeployment.
*/
public static void clearLocks() {
synchronized (locks) {
locks.clear();
}
}
protected void after(ActionInvocation invocation, String result) throws Exception {
Map<String, Object> session = ActionContext.getContext().getSession();
if ( session != null) {
@@ -29,19 +29,10 @@ import org.apache.struts2.util.reflection.ReflectionProvider;
import java.util.Map;
/**
* Interceptors marked with this interface support dynamic parameter evaluation at action invocation time.
* Parameters are set during interceptor creation (factory time), then re-evaluated during each action
* invocation to resolve expressions like ${someValue}.
* <p>
* This enables both:
* <ul>
* <li>Static configuration in interceptor stacks (e.g., allowedTypes="image/png,image/jpeg")</li>
* <li>Dynamic expressions evaluated per-request (e.g., maximumSize="${maxUploadSize}")</li>
* </ul>
* <p>
* The {@link Interceptor#init()} method is called after initial parameter setting, so interceptors
* can rely on configured values during initialization. Expression parameters (containing ${...})
* are re-evaluated at invocation time via {@link LazyParamInjector}.
* Interceptors marked with this interface won't be fully initialised during initialisation.
* Appropriated params will be injected just before usage of the interceptor.
*
* Please be aware that in such case {@link Interceptor#init()} method must be prepared for this.
*
* @since 2.5.9
*/
@@ -77,7 +68,7 @@ public interface WithLazyParams {
public Interceptor injectParams(Interceptor interceptor, Map<String, String> params, ActionContext invocationContext) {
for (Map.Entry<String, String> entry : params.entrySet()) {
Object paramValue = textParser.evaluate(new char[]{'$'}, entry.getValue(), valueEvaluator, TextParser.DEFAULT_LOOP_COUNT);
Object paramValue = textParser.evaluate(new char[]{ '$' }, entry.getValue(), valueEvaluator, TextParser.DEFAULT_LOOP_COUNT);
ognlUtil.setProperty(entry.getKey(), paramValue, interceptor, invocationContext.getContextMap());
}
return interceptor;
@@ -90,8 +90,7 @@ public class HttpMethodInterceptor extends AbstractInterceptor {
invocation.getProxy().getMethod(), AllowedHttpMethod.class.getSimpleName(), request.getMethod());
return doIntercept(invocation, method);
}
}
if (AnnotationUtils.isAnnotatedBy(action.getClass(), HTTP_METHOD_ANNOTATIONS)) {
} else if (AnnotationUtils.isAnnotatedBy(action.getClass(), HTTP_METHOD_ANNOTATIONS)) {
LOG.debug("Action: {} annotated with: {}, checking if request: {} meets allowed methods!",
action, AllowedHttpMethod.class.getSimpleName(), request.getMethod());
return doIntercept(invocation, action.getClass());
@@ -1,49 +0,0 @@
/*
* 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.struts2.interceptor.i18n;
import org.apache.struts2.ActionInvocation;
import org.apache.struts2.dispatcher.Parameter;
import java.util.Locale;
public abstract class AbstractLocaleHandler implements LocaleHandler {
protected final ActionInvocation actionInvocation;
private boolean shouldStore = true;
protected AbstractLocaleHandler(ActionInvocation invocation) {
this.actionInvocation = invocation;
}
@Override
public boolean shouldStore() {
return shouldStore;
}
protected void disableStore() {
this.shouldStore = false;
}
protected abstract Locale getLocaleFromParam(String requestedLocale);
protected abstract Parameter findLocaleParameter(ActionInvocation invocation, String parameterName);
protected abstract boolean isLocaleSupported(Locale locale);
}
@@ -1,81 +0,0 @@
/*
* 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.struts2.interceptor.i18n;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.apache.struts2.ActionInvocation;
import org.apache.struts2.dispatcher.Parameter;
import java.util.Locale;
import java.util.Set;
public abstract class AbstractStoredLocaleHandler extends AcceptLanguageLocaleHandler {
private static final Logger LOG = LogManager.getLogger(AbstractStoredLocaleHandler.class);
private final String explicitParameterName;
protected AbstractStoredLocaleHandler(ActionInvocation invocation,
String requestOnlyParameterName,
Set<Locale> supportedLocale,
String explicitParameterName) {
super(invocation, requestOnlyParameterName, supportedLocale);
this.explicitParameterName = explicitParameterName;
}
protected Locale findExplicitLocale() {
LOG.debug("Searching locale in request under parameter {}", explicitParameterName);
Parameter requestedLocale = findLocaleParameter(actionInvocation, explicitParameterName);
if (requestedLocale.isDefined()) {
Locale locale = getLocaleFromParam(requestedLocale.getValue());
if (locale != null && isLocaleSupported(locale)) {
return locale;
}
LOG.debug("Requested locale {} is not supported, ignoring", requestedLocale.getValue());
}
return null;
}
protected Locale findRequestOnlyLocale() {
Locale requestOnlyLocale = findRequestOnlyParamLocale();
if (requestOnlyLocale != null) {
LOG.debug("Found locale under request only param, it won't be stored!");
disableStore();
return requestOnlyLocale;
}
return null;
}
protected Locale normalizeStoredLocale(Locale locale, ActionInvocation invocation) {
if (locale != null && !isLocaleSupported(locale)) {
LOG.debug("Stored locale {} is not in supportedLocale, ignoring", locale);
locale = null;
}
if (locale == null) {
LOG.debug("No Locale defined in storage, fetching from current request and it won't be stored!");
disableStore();
return super.read(invocation);
} else {
LOG.debug("Found stored Locale {}, using it!", locale);
return locale;
}
}
}
@@ -1,78 +0,0 @@
/*
* 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.struts2.interceptor.i18n;
import org.apache.struts2.ActionInvocation;
import java.util.Enumeration;
import java.util.Locale;
import java.util.Set;
/**
* Resolves locale by first checking the request-only parameter and then falling back
* to the browser's {@code Accept-Language} header.
* <p>
* When a {@code supportedLocale} set is configured, only Accept-Language values present
* in that set are accepted. When the set is empty (the default), the first locale
* advertised by the browser is returned as-is.
*
* @see RequestLocaleHandler
* @see AbstractStoredLocaleHandler
*/
public abstract class AcceptLanguageLocaleHandler extends RequestLocaleHandler {
private final Set<Locale> supportedLocale;
protected AcceptLanguageLocaleHandler(ActionInvocation invocation, String requestOnlyParameterName, Set<Locale> supportedLocale) {
super(invocation, requestOnlyParameterName);
this.supportedLocale = supportedLocale;
}
@Override
public Locale find() {
Locale locale = findRequestOnlyParamLocale();
if (locale != null) {
return locale;
}
return findAcceptLanguageLocale();
}
@Override
public Locale read(ActionInvocation invocation) {
if (!supportedLocale.isEmpty()) {
Locale locale = findAcceptLanguageLocale();
if (locale != null) {
return locale;
}
}
return super.read(invocation);
}
@SuppressWarnings("rawtypes")
protected Locale findAcceptLanguageLocale() {
Enumeration locales = actionInvocation.getInvocationContext().getServletRequest().getLocales();
while (locales.hasMoreElements()) {
Locale acceptLocale = (Locale) locales.nextElement();
if (supportedLocale.isEmpty() || supportedLocale.contains(acceptLocale)) {
return acceptLocale;
}
}
return null;
}
}
@@ -1,78 +0,0 @@
/*
* 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.struts2.interceptor.i18n;
import org.apache.struts2.ActionInvocation;
import org.apache.struts2.ServletActionContext;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletResponse;
import java.util.Locale;
import java.util.Set;
public abstract class CookieLocaleHandler extends AbstractStoredLocaleHandler {
private final String attributeName;
protected CookieLocaleHandler(ActionInvocation invocation,
String requestOnlyParameterName,
Set<Locale> supportedLocale,
String requestCookieParameterName,
String attributeName) {
super(invocation, requestOnlyParameterName, supportedLocale, requestCookieParameterName);
this.attributeName = attributeName;
}
@Override
public Locale find() {
Locale locale = findExplicitLocale();
if (locale != null) {
return locale;
}
return findRequestOnlyLocale();
}
@Override
public Locale store(ActionInvocation invocation, Locale locale) {
HttpServletResponse response = ServletActionContext.getResponse();
Cookie cookie = new Cookie(attributeName, locale.toString());
cookie.setMaxAge(1209600); // two weeks
response.addCookie(cookie);
return locale;
}
@Override
public Locale read(ActionInvocation invocation) {
Locale locale = null;
Cookie[] cookies = ServletActionContext.getRequest().getCookies();
if (cookies != null) {
for (Cookie cookie : cookies) {
if (attributeName.equals(cookie.getName())) {
locale = getLocaleFromParam(cookie.getValue());
}
}
}
return normalizeStoredLocale(locale, invocation);
}
}
@@ -1,63 +0,0 @@
/*
* 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.struts2.interceptor.i18n;
import org.apache.struts2.ActionInvocation;
import java.util.Locale;
/**
* Strategy used by {@code I18nInterceptor} to resolve and optionally persist the current request locale.
* <p>
* Implementations encapsulate locale source-specific behavior (request parameters, session, cookies,
* or Accept-Language header), while the interceptor orchestrates the overall lifecycle.
*/
public interface LocaleHandler {
/**
* Looks for an explicit locale override in request-scoped sources.
*
* @return a locale override or {@code null} when no explicit override is present
*/
Locale find();
/**
* Reads locale from persistent/context sources when {@link #find()} did not resolve one.
*
* @param invocation current action invocation
* @return resolved locale or {@code null} when no locale could be resolved
*/
Locale read(ActionInvocation invocation);
/**
* Persists the resolved locale when storage is enabled for the current handler.
*
* @param invocation current action invocation
* @param locale locale to store
* @return the effective locale to apply to the invocation context
*/
Locale store(ActionInvocation invocation, Locale locale);
/**
* Indicates if the locale should be persisted for the current request.
*
* @return {@code true} when {@link #store(ActionInvocation, Locale)} should be invoked
*/
boolean shouldStore();
}
@@ -1,88 +0,0 @@
/*
* 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.struts2.interceptor.i18n;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.apache.struts2.ActionInvocation;
import org.apache.struts2.dispatcher.Parameter;
import java.util.Locale;
/**
* Resolves locale from a request-only parameter (not persisted to session or cookie).
* <p>
* When a matching request parameter is present and the locale is
* {@linkplain #isLocaleSupported(Locale) supported}, it is applied to the current
* request only; it is never stored for subsequent requests.
*
* @see AcceptLanguageLocaleHandler
* @see AbstractStoredLocaleHandler
*/
public abstract class RequestLocaleHandler extends AbstractLocaleHandler {
private static final Logger LOG = LogManager.getLogger(RequestLocaleHandler.class);
private final String requestOnlyParameterName;
protected RequestLocaleHandler(ActionInvocation invocation, String requestOnlyParameterName) {
super(invocation);
this.requestOnlyParameterName = requestOnlyParameterName;
}
@Override
public Locale find() {
return findRequestOnlyParamLocale();
}
/**
* Looks up the locale from the request-only parameter without any additional fallback.
* Subclasses that add fallback logic (e.g. Accept-Language) can override {@link #find()}
* while stored-locale handlers can call this method directly to skip the fallback.
*/
protected Locale findRequestOnlyParamLocale() {
LOG.debug("Searching locale in request under parameter {}", requestOnlyParameterName);
Parameter requestedLocale = findLocaleParameter(actionInvocation, requestOnlyParameterName);
if (requestedLocale.isDefined()) {
Locale locale = getLocaleFromParam(requestedLocale.getValue());
if (locale != null && isLocaleSupported(locale)) {
return locale;
}
LOG.debug("Requested locale {} is not supported, ignoring", requestedLocale.getValue());
}
return null;
}
@Override
public Locale store(ActionInvocation invocation, Locale locale) {
return locale;
}
@Override
public Locale read(ActionInvocation invocation) {
LOG.debug("Searching current Invocation context");
Locale locale = invocation.getInvocationContext().getLocale();
if (locale != null) {
LOG.debug("Applied invocation context locale: {}", locale);
}
return locale;
}
}
@@ -1,90 +0,0 @@
/*
* 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.struts2.interceptor.i18n;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.apache.struts2.ActionInvocation;
import org.apache.struts2.ServletActionContext;
import jakarta.servlet.http.HttpSession;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
public abstract class SessionLocaleHandler extends AbstractStoredLocaleHandler {
private static final Logger LOG = LogManager.getLogger(SessionLocaleHandler.class);
private final String attributeName;
protected SessionLocaleHandler(ActionInvocation invocation,
String requestOnlyParameterName,
Set<Locale> supportedLocale,
String parameterName,
String attributeName) {
super(invocation, requestOnlyParameterName, supportedLocale, parameterName);
this.attributeName = attributeName;
}
@Override
public Locale find() {
Locale locale = findExplicitLocale();
if (locale != null) {
return locale;
}
return findRequestOnlyLocale();
}
@Override
public Locale store(ActionInvocation invocation, Locale locale) {
Map<String, Object> session = invocation.getInvocationContext().getSession();
if (session != null) {
String sessionId = ServletActionContext.getRequest().getSession().getId();
synchronized (sessionId.intern()) {
session.put(attributeName, locale);
}
}
return locale;
}
@Override
public Locale read(ActionInvocation invocation) {
Locale locale = null;
LOG.debug("Checks session for saved locale");
HttpSession session = ServletActionContext.getRequest().getSession(false);
if (session != null) {
String sessionId = session.getId();
synchronized (sessionId.intern()) {
Object sessionLocale = invocation.getInvocationContext().getSession().get(attributeName);
if (sessionLocale instanceof Locale) {
locale = (Locale) sessionLocale;
LOG.debug("Applied session locale: {}", locale);
}
}
}
return normalizeStoredLocale(locale, invocation);
}
}
@@ -1,192 +0,0 @@
/*
* 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.struts2.interceptor.parameter;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.apache.struts2.inject.Inject;
import org.apache.struts2.ognl.OgnlUtil;
import org.apache.struts2.ognl.ThreadAllowlist;
import org.apache.struts2.util.ProxyService;
import java.beans.BeanInfo;
import java.beans.IntrospectionException;
import java.beans.PropertyDescriptor;
import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.Arrays;
import java.util.Optional;
import static org.apache.commons.lang3.StringUtils.indexOfAny;
import static org.apache.struts2.security.DefaultAcceptedPatternsChecker.NESTING_CHARS;
import static org.apache.struts2.security.DefaultAcceptedPatternsChecker.NESTING_CHARS_STR;
/**
* Default {@link ParameterAllowlister}. Registers the root property's class (and generic type args for {@code depth >= 2})
* into the OGNL {@link ThreadAllowlist} so OGNL may introspect and traverse a nested path on the value stack. Logic is
* extracted verbatim from {@code ParametersInterceptor.performOgnlAllowlisting} so the OGNL parameter and cookie
* channels share a single implementation.
*
* <p>No-ops when:
* <ul>
* <li>{@code paramDepth == 0} shallow setter; OGNL does not need to traverse</li>
* <li>the root property has no {@code @StrutsParameter} annotation reachable via {@link java.beans.PropertyDescriptor}
* or as a public field (e.g. a {@code ModelDriven} model whose properties are not individually annotated). A
* {@code LOG.debug} surfaces this case so the gap between authorization and OGNL traversal is observable.</li>
* </ul>
*
* @since 7.2.0
*/
public class OgnlParameterAllowlister implements ParameterAllowlister {
private static final Logger LOG = LogManager.getLogger(OgnlParameterAllowlister.class);
private OgnlUtil ognlUtil;
private ProxyService proxyService;
private ThreadAllowlist threadAllowlist;
@Inject
public void setOgnlUtil(OgnlUtil ognlUtil) {
this.ognlUtil = ognlUtil;
}
@Inject
public void setProxyService(ProxyService proxyService) {
this.proxyService = proxyService;
}
@Inject
public void setThreadAllowlist(ThreadAllowlist threadAllowlist) {
this.threadAllowlist = threadAllowlist;
}
@Override
public void primeAllowlistForPath(String parameterName, Object target) {
if (parameterName == null || parameterName.isEmpty() || target == null) {
return;
}
long paramDepth = parameterName.codePoints().mapToObj(c -> (char) c).filter(NESTING_CHARS::contains).count();
if (paramDepth == 0) {
return;
}
int nestingIndex = indexOfAny(parameterName, NESTING_CHARS_STR);
String rootProperty = nestingIndex == -1 ? parameterName : parameterName.substring(0, nestingIndex);
String normalisedRootProperty = Character.toLowerCase(rootProperty.charAt(0)) + rootProperty.substring(1);
if (allowlistViaPropertyDescriptor(target, normalisedRootProperty, paramDepth)) {
return;
}
if (allowlistViaPublicField(target, normalisedRootProperty, paramDepth)) {
return;
}
// Authorization passed but no @StrutsParameter on the root property e.g. ModelDriven model with no
// per-property annotations. OGNL won't be able to walk this nested path; surface the gap in logs.
LOG.debug("Parameter [{}] authorized but no @StrutsParameter on root property [{}] of [{}]; "
+ "OGNL allowlist not primed and nested traversal may be blocked",
parameterName, normalisedRootProperty, ultimateClass(target).getSimpleName());
}
private boolean allowlistViaPropertyDescriptor(Object target, String rootProperty, long paramDepth) {
BeanInfo beanInfo = getBeanInfo(target);
if (beanInfo == null) {
return false;
}
Optional<PropertyDescriptor> propDescOpt = Arrays.stream(beanInfo.getPropertyDescriptors())
.filter(desc -> desc.getName().equals(rootProperty)).findFirst();
if (propDescOpt.isEmpty()) {
return false;
}
PropertyDescriptor propDesc = propDescOpt.get();
Method relevantMethod = propDesc.getReadMethod();
if (relevantMethod == null || getPermittedInjectionDepth(relevantMethod) < paramDepth) {
return false;
}
allowlistClass(propDesc.getPropertyType());
if (paramDepth >= 2) {
allowlistParameterizedTypeArg(relevantMethod.getGenericReturnType());
}
return true;
}
private boolean allowlistViaPublicField(Object target, String rootProperty, long paramDepth) {
Class<?> targetClass = ultimateClass(target);
Field field;
try {
field = targetClass.getDeclaredField(rootProperty);
} catch (NoSuchFieldException e) {
return false;
}
if (!Modifier.isPublic(field.getModifiers()) || getPermittedInjectionDepth(field) < paramDepth) {
return false;
}
allowlistClass(field.getType());
if (paramDepth >= 2) {
allowlistParameterizedTypeArg(field.getGenericType());
}
return true;
}
private void allowlistClass(Class<?> clazz) {
threadAllowlist.allowClassHierarchy(clazz);
}
private void allowlistParameterizedTypeArg(Type genericType) {
if (!(genericType instanceof ParameterizedType pType)) {
return;
}
Type[] paramTypes = pType.getActualTypeArguments();
allowlistParamType(paramTypes[0]);
if (paramTypes.length > 1) {
allowlistParamType(paramTypes[1]);
}
}
private void allowlistParamType(Type paramType) {
if (paramType instanceof Class<?> clazz) {
allowlistClass(clazz);
}
}
private int getPermittedInjectionDepth(AnnotatedElement element) {
StrutsParameter annotation = element.getAnnotation(StrutsParameter.class);
return annotation == null ? -1 : annotation.depth();
}
private Class<?> ultimateClass(Object target) {
if (proxyService.isProxy(target)) {
return proxyService.ultimateTargetClass(target);
}
return target.getClass();
}
private BeanInfo getBeanInfo(Object target) {
Class<?> targetClass = ultimateClass(target);
try {
return ognlUtil.getBeanInfo(targetClass);
} catch (IntrospectionException e) {
LOG.warn("Error introspecting target {} for OGNL allowlisting", targetClass, e);
return null;
}
}
}
@@ -1,44 +0,0 @@
/*
* 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.struts2.interceptor.parameter;
/**
* Primes channel-specific runtime state required for an already-authorized parameter path to be walked by the
* value-stack for example, registering the path's classes into the OGNL {@link org.apache.struts2.ognl.ThreadAllowlist}
* so OGNL may traverse them. Separated from {@link ParameterAuthorizer} so the authorization decision can remain
* side-effect-free and reusable from non-OGNL channels (Jackson, Juneau).
*
* <p>Implementations MUST NOT repeat the authorization decision that is owned by
* {@link ParameterAuthorizer#isAuthorized}. A no-op return (e.g. shallow paths, unannotated root) means "no priming
* needed or possible" and never "rejected": callers must not treat the absence of priming as a negative authorization
* signal.</p>
*
* @since 7.2.0
*/
public interface ParameterAllowlister {
/**
* Primes the channel-specific allowlist for an authorized parameter path. Side-effect-only; no return value
* because a no-op is a valid outcome (see class-level javadoc).
*
* @param parameterName the parameter name (e.g. {@code "user.role"}, {@code "items[0].name"})
* @param target the object receiving the parameter value (the action, or the model for ModelDriven actions)
*/
void primeAllowlistForPath(String parameterName, Object target);
}
@@ -1,144 +0,0 @@
/*
* 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.struts2.interceptor.parameter;
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.Objects;
/**
* ThreadLocal holder for per-request parameter authorization state, used by deserializer-level
* authorization (e.g. the REST plugin's {@code ContentTypeInterceptor}). All state the
* {@link ParameterAuthorizer}, the target, the action, and the current property-path stack is
* bound by input-channel interceptors before invoking the deserializer, and unbound in a
* {@code finally} block afterwards.
*
* <p>Implementations that consult this context (e.g. {@code AuthorizingSettableBeanProperty}) call
* {@link #isActive()} to decide whether to enforce authorization at all when no context is bound
* (default config, {@code requireAnnotations=false}), they short-circuit to the delegate behavior.</p>
*
* @since 7.2.0
*/
public final class ParameterAuthorizationContext {
private static final ThreadLocal<State> STATE = new ThreadLocal<>();
private static final ThreadLocal<Deque<String>> PATH_STACK = ThreadLocal.withInitial(ArrayDeque::new);
private ParameterAuthorizationContext() {
// utility
}
/**
* Binds an authorizer, target, and action to the current thread. {@code target} is the object
* being populated typically the action itself, or the model object for {@code ModelDriven}
* actions (the same contract as {@link ParameterAuthorizer#isAuthorized}). {@code action} is
* always the action instance. A subsequent call without an intervening {@link #unbind()} replaces
* the prior state without resetting the path stack.
*
* @param authorizer the authorizer to use for this request; must not be {@code null}
* @param target the object being populated (action or model)
* @param action the action instance
*/
public static void bind(ParameterAuthorizer authorizer, Object target, Object action) {
Objects.requireNonNull(authorizer, "authorizer");
STATE.set(new State(authorizer, target, action));
}
/**
* Removes the bound authorizer state and clears the path stack for the current thread.
* Safe to call even when no context has been bound.
*/
public static void unbind() {
STATE.remove();
PATH_STACK.remove();
}
/**
* Returns {@code true} if an authorizer has been bound on the current thread via {@link #bind}.
*/
public static boolean isActive() {
return STATE.get() != null;
}
/**
* Authorizes a parameter at the given path against the bound authorizer. Returns {@code true}
* when no context is bound callers that don't want enforcement at all should not bind context
* in the first place; this default keeps wrapping deserializers safe for non-authorized requests.
*/
public static boolean isAuthorized(String parameterPath) {
State state = STATE.get();
if (state == null) {
return true;
}
return state.authorizer.isAuthorized(parameterPath, state.target, state.action);
}
/**
* Pushes the full cumulative path prefix onto the stack. Subsequent {@link #pathFor(String)}
* calls will append {@code name} to this prefix. Callers building a collection-element prefix
* (e.g. {@code items[0]}) must pass the full string including the suffix.
*
* @param cumulativePath the full path prefix to push (e.g. {@code "address"} or {@code "items[0]"})
*/
public static void pushPath(String cumulativePath) {
PATH_STACK.get().push(cumulativePath);
}
/**
* Pops the top path prefix from the stack. Has no effect if the stack is empty.
*/
public static void popPath() {
Deque<String> stack = PATH_STACK.get();
if (!stack.isEmpty()) {
stack.pop();
}
}
/**
* @return the current top-of-stack path prefix, or empty string if none
*/
public static String currentPathPrefix() {
Deque<String> stack = PATH_STACK.get();
if (stack.isEmpty()) {
return "";
}
return stack.peek();
}
/**
* Builds the full path for a property at the current nesting level: {@code prefix.propertyName}
* (or just {@code propertyName} when at the root).
*/
public static String pathFor(String propertyName) {
String prefix = currentPathPrefix();
return prefix.isEmpty() ? propertyName : prefix + "." + propertyName;
}
private static final class State {
final ParameterAuthorizer authorizer;
final Object target;
final Object action;
State(ParameterAuthorizer authorizer, Object target, Object action) {
this.authorizer = authorizer;
this.target = target;
this.action = action;
}
}
}
@@ -1,68 +0,0 @@
/*
* 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.struts2.interceptor.parameter;
/**
* Service for determining whether a given parameter name is authorized for injection into a target object, based on
* {@link StrutsParameter} annotation presence and depth.
*
* <p>This service extracts the authorization logic from {@link ParametersInterceptor} so that it can be reused by other
* input channels (e.g. JSON plugin, REST plugin) that also need to enforce {@code @StrutsParameter} rules.</p>
*
* <p>Implementations must NOT perform OGNL ThreadAllowlist side effects those remain specific to
* {@link ParametersInterceptor}.</p>
*
* @since 7.2.0
*/
public interface ParameterAuthorizer {
/**
* Determines whether a parameter with the given name is authorized for injection into the given target object.
*
* <p>When {@code struts.parameters.requireAnnotations} is {@code false}, this method always returns {@code true}
* for backward compatibility.</p>
*
* @param parameterName the parameter name (e.g. "name", "address.city", "items[0].name")
* @param target the object receiving the parameter value (the action, or the model for ModelDriven actions)
* @param action the action instance; used to detect ModelDriven exemption (when {@code target != action},
* the target is the model and is exempt from annotation requirements)
* @return {@code true} if the parameter is authorized for injection, {@code false} otherwise
*/
boolean isAuthorized(String parameterName, Object target, Object action);
/**
* Resolves the target object whose annotations should be checked for authorization.
* For {@link org.apache.struts2.ModelDriven} actions, the default implementation returns the action itself;
* the production implementation ({@link StrutsParameterAuthorizer}) overrides this to return the model from
* the value stack.
*
* <p>Callers that need both authorization checks AND the resolved target (e.g. for downstream OGNL allowlisting)
* should call this once and reuse the result.</p>
*
* <p>This is a {@code default} method to preserve the interface as a functional interface (SAM) for
* lambda-based test stubs.</p>
*
* @param action the action instance
* @return the resolved target either the action or its model
* @since 7.2.0
*/
default Object resolveTarget(Object action) {
return action;
}
}
@@ -23,6 +23,7 @@ import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.apache.struts2.ActionContext;
import org.apache.struts2.ActionInvocation;
import org.apache.struts2.ModelDriven;
import org.apache.struts2.StrutsConstants;
import org.apache.struts2.action.NoParameters;
import org.apache.struts2.action.ParameterNameAware;
@@ -38,7 +39,7 @@ import org.apache.struts2.security.DefaultAcceptedPatternsChecker;
import org.apache.struts2.security.ExcludedPatternsChecker;
import org.apache.struts2.util.ClearableValueStack;
import org.apache.struts2.util.MemberAccessValueStack;
import org.apache.struts2.util.ProxyService;
import org.apache.struts2.util.ProxyUtil;
import org.apache.struts2.util.TextParseUtil;
import org.apache.struts2.util.ValueStack;
import org.apache.struts2.util.ValueStackFactory;
@@ -65,7 +66,10 @@ import java.util.regex.Pattern;
import static java.lang.String.format;
import static java.util.Collections.unmodifiableSet;
import static java.util.stream.Collectors.joining;
import static org.apache.commons.lang3.StringUtils.indexOfAny;
import static org.apache.commons.lang3.StringUtils.normalizeSpace;
import static org.apache.struts2.security.DefaultAcceptedPatternsChecker.NESTING_CHARS;
import static org.apache.struts2.security.DefaultAcceptedPatternsChecker.NESTING_CHARS_STR;
import static org.apache.struts2.util.DebugUtils.logWarningForFirstOccurrence;
import static org.apache.struts2.util.DebugUtils.notifyDeveloperOfError;
@@ -91,13 +95,10 @@ public class ParametersInterceptor extends MethodFilterInterceptor {
private ValueStackFactory valueStackFactory;
private OgnlUtil ognlUtil;
protected ThreadAllowlist threadAllowlist;
private ProxyService proxyService;
private ExcludedPatternsChecker excludedPatterns;
private AcceptedPatternsChecker acceptedPatterns;
private Set<Pattern> excludedValuePatterns = null;
private Set<Pattern> acceptedValuePatterns = null;
private ParameterAuthorizer parameterAuthorizer;
private transient ParameterAllowlister parameterAllowlister;
@Inject
public void setValueStackFactory(ValueStackFactory valueStackFactory) {
@@ -114,21 +115,6 @@ public class ParametersInterceptor extends MethodFilterInterceptor {
this.threadAllowlist = threadAllowlist;
}
@Inject
public void setProxyService(ProxyService proxyService) {
this.proxyService = proxyService;
}
@Inject
public void setParameterAuthorizer(ParameterAuthorizer parameterAuthorizer) {
this.parameterAuthorizer = parameterAuthorizer;
}
@Inject
public void setParameterAllowlister(ParameterAllowlister parameterAllowlister) {
this.parameterAllowlister = parameterAllowlister;
}
@Inject(StrutsConstants.STRUTS_DEVMODE)
public void setDevMode(String mode) {
this.devMode = BooleanUtils.toBoolean(mode);
@@ -360,9 +346,6 @@ public class ParametersInterceptor extends MethodFilterInterceptor {
* Checks if the Action class member corresponding to a parameter is appropriately annotated with
* {@link StrutsParameter} and OGNL allowlists any necessary classes.
* <p>
* Authorization is delegated to {@link ParameterAuthorizer}. If authorized, OGNL allowlisting is performed as a
* second pass (this is specific to the OGNL-based parameter injection path and not shared with other input channels).
* <p>
* Note that this logic relies on the use of {@link DefaultAcceptedPatternsChecker#NESTING_CHARS} and may also
* be adversely impacted by the use of custom OGNL property accessors.
*/
@@ -371,15 +354,23 @@ public class ParametersInterceptor extends MethodFilterInterceptor {
return true;
}
Object target = parameterAuthorizer.resolveTarget(action);
long paramDepth = name.codePoints().mapToObj(c -> (char) c).filter(NESTING_CHARS::contains).count();
// Delegate authorization check to shared ParameterAuthorizer (no OGNL side effects)
if (!parameterAuthorizer.isAuthorized(name, target, action)) {
return false;
if (action instanceof ModelDriven<?> && !ActionContext.getContext().getValueStack().peek().equals(action)) {
LOG.debug("Model driven Action detected, exempting from @StrutsParameter annotation requirement");
return true;
}
parameterAllowlister.primeAllowlistForPath(name, target);
return true;
if (requireAnnotationsTransitionMode && paramDepth == 0) {
LOG.debug("Annotation transition mode enabled, exempting non-nested parameter [{}] from @StrutsParameter annotation requirement", name);
return true;
}
int nestingIndex = indexOfAny(name, NESTING_CHARS_STR);
String rootProperty = nestingIndex == -1 ? name : name.substring(0, nestingIndex);
String normalisedRootProperty = Character.toLowerCase(rootProperty.charAt(0)) + rootProperty.substring(1);
return hasValidAnnotatedMember(normalisedRootProperty, action, paramDepth);
}
/**
@@ -525,8 +516,8 @@ public class ParametersInterceptor extends MethodFilterInterceptor {
}
protected Class<?> ultimateClass(Object action) {
if (proxyService.isProxy(action)) {
return proxyService.ultimateTargetClass(action);
if (ProxyUtil.isProxy(action)) {
return ProxyUtil.ultimateTargetClass(action);
}
return action.getClass();
}

Some files were not shown because too many files have changed in this diff Show More