mirror of
https://github.com/apache/struts.git
synced 2026-08-11 01:27:14 +00:00
Compare commits
63 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b039dc5079 | |||
| 6374e31384 | |||
| 803b5cbbd0 | |||
| 1dda92ed23 | |||
| cda38c79db | |||
| fbb904e9c6 | |||
| f7923fc704 | |||
| 47718f5a8a | |||
| e2e8fa1f33 | |||
| 71c204f04b | |||
| d276b2dded | |||
| 620fcbd152 | |||
| 41abbd1684 | |||
| b3ba3d04d5 | |||
| c9918bf8bb | |||
| 2215b6873c | |||
| b0d6d4f2d4 | |||
| da5908d0ea | |||
| 20b20e47eb | |||
| f132a2e43b | |||
| 556dece531 | |||
| 8ac63e535a | |||
| 311cda1681 | |||
| 4f1e491ae8 | |||
| afa33e1b09 | |||
| 572f3aaf69 | |||
| 47d46f7013 | |||
| 55e268b009 | |||
| ef00dd07a9 | |||
| 26fede63ca | |||
| b5d93f6860 | |||
| 944ad2f1e3 | |||
| 8df3a4be13 | |||
| 121bb1ad22 | |||
| 4c94c4f89a | |||
| 507c64e49a | |||
| 246a2507bb | |||
| 4af368720e | |||
| 28696ce1a7 | |||
| 32a7789485 | |||
| 4d96950c51 | |||
| 4d2eb93835 | |||
| 8ac1511290 | |||
| ae94fd0d5d | |||
| 3fc9efce5a | |||
| ca740ed8fb | |||
| a9ce3e3c99 | |||
| fa34d2b02c | |||
| a21c763d8a | |||
| 2527ff15ff | |||
| 1eae40b817 | |||
| f4d4ffe485 | |||
| 182c00c083 | |||
| d2810d42f0 | |||
| 71f25438e5 | |||
| b704cb942e | |||
| c90bb70c25 | |||
| a0a213f7c5 | |||
| e3d09dfc47 | |||
| accd4c7e5a | |||
| ea906a22e4 | |||
| fe69771562 | |||
| 11bc215c94 |
@@ -1,330 +0,0 @@
|
||||
---
|
||||
name: test-runner
|
||||
description: Execute and analyze Maven tests for Apache Struts. Use PROACTIVELY when code changes, user mentions testing, asks to run tests, check test coverage, or validate changes. MUST BE USED for all Maven test execution (mvn test), test result analysis, and coverage reports.
|
||||
tools: bash_tool, view, str_replace
|
||||
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.
|
||||
@@ -29,9 +29,10 @@ updates:
|
||||
- dependency-name: "opensymphony:sitemesh"
|
||||
- dependency-name: "net.sf.jasperreports:jasperreports"
|
||||
- dependency-name: "javax.enterprise:cdi-api"
|
||||
- dependency-name: "org.springframework:spring-core"
|
||||
- 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"
|
||||
|
||||
@@ -52,12 +52,12 @@ jobs:
|
||||
java-version: 17
|
||||
cache: 'maven'
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v4.32.1
|
||||
uses: github/codeql-action/init@v4.35.1
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@v4.32.1
|
||||
uses: github/codeql-action/autobuild@v4.35.1
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v4.32.1
|
||||
uses: github/codeql-action/analyze@v4.35.1
|
||||
with:
|
||||
category: "/language:${{matrix.language}}"
|
||||
|
||||
@@ -58,13 +58,13 @@ jobs:
|
||||
publish_results: true
|
||||
|
||||
- name: "Upload artifact"
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # 6.0.0
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # 7.0.0
|
||||
with:
|
||||
name: SARIF file
|
||||
path: results.sarif
|
||||
retention-days: 5
|
||||
|
||||
- name: "Upload to code-scanning"
|
||||
uses: github/codeql-action/upload-sarif@ab5b0e3aabf4de044f07a63754c2110d3ef2df38 # 2.22.11
|
||||
uses: github/codeql-action/upload-sarif@c618c9bddbf8ce520050acf14e9bb6c220e22931 # 2.22.11
|
||||
with:
|
||||
sarif_file: results.sarif
|
||||
|
||||
@@ -49,3 +49,8 @@ test-output
|
||||
|
||||
# Claude Code local settings
|
||||
.claude/settings.local.json
|
||||
|
||||
# Cursor + Metals
|
||||
.cursor/
|
||||
.bloop/
|
||||
.metals/
|
||||
|
||||
@@ -6,38 +6,19 @@ For detailed procedures, use the specialized agents and commands in `.claude/age
|
||||
|
||||
## Project Overview
|
||||
|
||||
Apache Struts is a mature MVC web application framework for Java (originally WebWork 2). Current version: *
|
||||
*7.2.0-SNAPSHOT**.
|
||||
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.
|
||||
|
||||
### Build Commands
|
||||
|
||||
```bash
|
||||
# Full build with tests
|
||||
mvn clean install
|
||||
|
||||
# Run all tests (faster, skips assembly)
|
||||
# Run tests (skip assembly for speed)
|
||||
mvn test -DskipAssembly
|
||||
|
||||
# Run single test class
|
||||
mvn test -DskipAssembly -Dtest=MyClassTest
|
||||
# Single test in specific module
|
||||
mvn test -DskipAssembly -pl core -Dtest=MyClassTest#testMethodName
|
||||
|
||||
# Run single test method
|
||||
mvn test -DskipAssembly -Dtest=MyClassTest#testMethodName
|
||||
|
||||
# Run tests in a specific module
|
||||
mvn test -DskipAssembly -pl core
|
||||
|
||||
# Build without tests
|
||||
mvn clean install -DskipTests
|
||||
|
||||
# Build with code coverage (JaCoCo)
|
||||
mvn clean install -Pcoverage
|
||||
|
||||
# Build with Jakarta EE 11 (Spring 7)
|
||||
# Jakarta EE 11 / Spring 7 profile
|
||||
mvn clean install -Pjakartaee11
|
||||
|
||||
# Run OWASP dependency vulnerability check
|
||||
mvn verify -Pdependency-check
|
||||
```
|
||||
|
||||
### Project Structure
|
||||
@@ -57,13 +38,6 @@ struts/
|
||||
|
||||
**Request Lifecycle**: `Dispatcher` → `ActionProxy` → `ActionInvocation` → Interceptor stack → `Action` → Result
|
||||
|
||||
Key components:
|
||||
|
||||
- **ActionSupport**: Base class for actions (validation, i18n, messages)
|
||||
- **ActionContext**: Thread-local context with request/response/session data
|
||||
- **Interceptors**: Cross-cutting concerns (validation, file upload, security, params)
|
||||
- **Results**: Response handlers (dispatcher, redirect, json, stream)
|
||||
|
||||
Key packages in `org.apache.struts2`:
|
||||
|
||||
- `dispatcher` - Request handling, `Dispatcher`, servlet integration
|
||||
@@ -72,22 +46,14 @@ Key packages in `org.apache.struts2`:
|
||||
- `action` - Action interfaces (`UploadedFilesAware`, `SessionAware`, etc.)
|
||||
- `security` - Security utilities and OGNL member access policies
|
||||
|
||||
### Technology Stack
|
||||
|
||||
- **Java 17+** with Jakarta EE 10 (Servlet 6.0, JSP 3.1)
|
||||
- **OGNL** - Expression language for value stack access
|
||||
- **FreeMarker** - Default template engine for UI tags
|
||||
- **Commons FileUpload2** - File upload handling
|
||||
- **Log4j2/SLF4J** - Logging
|
||||
|
||||
## Security-Critical Patterns
|
||||
|
||||
Apache Struts has a history of security vulnerabilities. Follow these strictly:
|
||||
Apache Struts has a history of security vulnerabilities (OGNL injection, temp file exploits). Apply these Struts-specific patterns:
|
||||
|
||||
1. **Temporary files**: Never use system temp directory; use UUID-based names in controlled locations
|
||||
2. **OGNL expressions**: Never evaluate user-controlled OGNL; use allowlist member access
|
||||
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 injection**: Use `ParameterNameAware` to filter dangerous parameter names
|
||||
4. **Parameter filtering**: Use `ParameterNameAware` to restrict accepted parameter names
|
||||
|
||||
```java
|
||||
// Secure temporary file pattern
|
||||
@@ -97,46 +63,12 @@ protected File createTemporaryFile(String fileName, Path location) {
|
||||
}
|
||||
```
|
||||
|
||||
Run `/security_scan` for comprehensive security analysis.
|
||||
|
||||
## Testing
|
||||
|
||||
**Priority order for running tests:**
|
||||
|
||||
1. **JetBrains MCP** (in IntelliJ): `mcp__jetbrains__execute_run_configuration`
|
||||
2. **test-runner agent**: `Task` tool with `subagent_type="test-runner"`
|
||||
3. **Direct Maven**: `mvn test -DskipAssembly -Dtest=TestClassName`
|
||||
|
||||
Tests use JUnit 5 with AssertJ assertions and Mockito for mocking.
|
||||
|
||||
## Available Tools
|
||||
|
||||
### Commands
|
||||
|
||||
- `/security_scan` - OGNL injection, CVE detection, security analysis
|
||||
- `/quality_check` - JavaDoc compliance, coding standards
|
||||
- `/config_analyze` - struts.xml validation, interceptor analysis
|
||||
- `/create_plan` / `/validate_plan` - Implementation planning
|
||||
- `/research_codebase` - Codebase exploration
|
||||
|
||||
### Specialized Agents
|
||||
|
||||
- `test-runner` - Maven test execution (use this to RUN tests)
|
||||
- `security-analyzer` - Security vulnerability scanning
|
||||
- `codebase-locator` - Find files, classes, implementations
|
||||
- `codebase-pattern-finder` - Find similar code patterns
|
||||
- `config-validator` - Validate Struts configuration files
|
||||
Tests use JUnit 5 with AssertJ assertions and Mockito for mocking. Run with `mvn test -DskipAssembly`.
|
||||
|
||||
## Pull Requests
|
||||
|
||||
- **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
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
1. Never use `File.createTempFile()` without controlling the directory
|
||||
2. Always clean up temporary files (track and delete in finally blocks)
|
||||
3. Test error paths and cleanup behavior, not just happy paths
|
||||
4. Don't catch generic `Exception` - catch specific types
|
||||
5. Use `protected` visibility for methods subclasses may override
|
||||
- **Issue tracker**: https://issues.apache.org/jira/projects/WW
|
||||
@@ -0,0 +1,18 @@
|
||||
# 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.
|
||||
@@ -1,15 +0,0 @@
|
||||
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.
|
||||
@@ -0,0 +1,19 @@
|
||||
# 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.
|
||||
@@ -1,16 +0,0 @@
|
||||
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.
|
||||
@@ -119,6 +119,10 @@
|
||||
<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>
|
||||
@@ -207,7 +211,7 @@
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-failsafe-plugin</artifactId>
|
||||
<version>3.5.4</version>
|
||||
<version>3.5.5</version>
|
||||
<configuration>
|
||||
<includes>
|
||||
<include>it.org.apache.struts2.showcase.*Test</include>
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
/*
|
||||
* 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();
|
||||
}
|
||||
}
|
||||
+31
-2
@@ -30,7 +30,9 @@ import java.io.BufferedReader;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.net.URL;
|
||||
import java.net.URI;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
@@ -90,7 +92,11 @@ public class ViewSourceAction extends ActionSupport implements ServletContextAwa
|
||||
if (config != null && config.startsWith("file:/")) {
|
||||
int pos = config.lastIndexOf(':');
|
||||
configLine = Integer.parseInt(config.substring(pos + 1));
|
||||
configLines = read(new URL(config.substring(0, pos)).openStream(), configLine);
|
||||
String fileUrl = config.substring(0, pos);
|
||||
Path configPath = resolveAllowedConfigPath(fileUrl);
|
||||
if (configPath != null) {
|
||||
configLines = read(Files.newInputStream(configPath), configLine);
|
||||
}
|
||||
}
|
||||
return SUCCESS;
|
||||
}
|
||||
@@ -227,6 +233,29 @@ 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;
|
||||
|
||||
@@ -20,21 +20,26 @@
|
||||
*/
|
||||
-->
|
||||
<!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>
|
||||
</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>
|
||||
|
||||
<!-- Spring AOP Proxied Action Chain Test (WW-5514) -->
|
||||
<action name="proxiedActionChain1" class="proxiedActionChain1">
|
||||
<result type="chain">actionChain2</result>
|
||||
</action>
|
||||
</package>
|
||||
</struts>
|
||||
|
||||
|
||||
|
||||
@@ -20,83 +20,88 @@
|
||||
*/
|
||||
-->
|
||||
<!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.enable" value="true"/>
|
||||
<constant name="struts.parameters.requireAnnotations" value="true"/>
|
||||
<constant name="struts.allowlist.packageNames" value="org.apache.struts2.showcase"/>
|
||||
|
||||
<constant name="struts.convention.package.locators.basePackage" value="org.apache.struts2.showcase" />
|
||||
<constant name="struts.convention.result.path" value="/WEB-INF" />
|
||||
<!-- 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"/>
|
||||
|
||||
<!-- 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>
|
||||
@@ -125,7 +130,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">
|
||||
@@ -146,9 +151,11 @@
|
||||
<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>
|
||||
@@ -168,5 +175,5 @@
|
||||
|
||||
</struts>
|
||||
|
||||
<!-- END SNIPPET: xworkSample -->
|
||||
<!-- END SNIPPET: xworkSample -->
|
||||
|
||||
|
||||
@@ -115,5 +115,25 @@
|
||||
<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>
|
||||
|
||||
|
||||
+67
@@ -0,0 +1,67 @@
|
||||
/*
|
||||
* 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"));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -93,9 +93,16 @@ public interface ActionProxy {
|
||||
String getMethod();
|
||||
|
||||
/**
|
||||
* Gets status of the method value's initialization.
|
||||
* 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>
|
||||
*
|
||||
* @return true if the method returned by getMethod() is not a default initializer value.
|
||||
* @return {@code true} if the method was explicitly provided or resolved from config;
|
||||
* {@code false} only when defaulting to {@code "execute"}
|
||||
*/
|
||||
boolean isMethodSpecified();
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -522,6 +522,35 @@ 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";
|
||||
|
||||
/**
|
||||
* Enables evaluation of OGNL expressions
|
||||
*
|
||||
@@ -707,6 +736,15 @@ 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}
|
||||
*/
|
||||
|
||||
@@ -49,17 +49,19 @@ 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;
|
||||
@@ -87,6 +89,8 @@ public class Checkbox extends UIBean {
|
||||
} else {
|
||||
addParameter(ATTR_SUBMIT_UNCHECKED, false);
|
||||
}
|
||||
|
||||
addParameter(ATTR_HIDDEN_PREFIX, hiddenPrefixGlobal);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -99,14 +103,19 @@ 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,6 +68,14 @@ 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;
|
||||
|
||||
@@ -61,6 +61,7 @@ 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;
|
||||
@@ -72,6 +73,7 @@ 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.util.ProxyService;
|
||||
import org.apache.struts2.util.TextParser;
|
||||
import org.apache.struts2.util.ValueStackFactory;
|
||||
import org.apache.struts2.util.location.LocatableProperties;
|
||||
@@ -442,6 +444,8 @@ 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(SecurityMemberAccess.class, StrutsConstants.STRUTS_MEMBER_ACCESS, builder, props, Scope.PROTOTYPE);
|
||||
alias(OgnlGuard.class, StrutsConstants.STRUTS_OGNL_GUARD, builder, props, Scope.SINGLETON);
|
||||
|
||||
@@ -85,13 +85,17 @@ 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.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;
|
||||
@@ -144,6 +148,8 @@ 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);
|
||||
}
|
||||
@@ -243,6 +249,10 @@ public class DefaultConfiguration implements Configuration {
|
||||
public void destroy() {
|
||||
packageContexts.clear();
|
||||
loadedFileNames.clear();
|
||||
if (container != null) {
|
||||
container.destroy();
|
||||
container = null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -260,8 +270,7 @@ public class DefaultConfiguration implements Configuration {
|
||||
*/
|
||||
@Override
|
||||
public synchronized List<PackageProvider> reloadContainer(List<ContainerProvider> providers) throws ConfigurationException {
|
||||
packageContexts.clear();
|
||||
loadedFileNames.clear();
|
||||
destroy();
|
||||
List<PackageProvider> packageProviders = new ArrayList<>();
|
||||
|
||||
ContainerProperties props = new ContainerProperties();
|
||||
@@ -395,6 +404,8 @@ 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(OgnlUtil.class, Scope.SINGLETON)
|
||||
.factory(SecurityMemberAccess.class, Scope.PROTOTYPE)
|
||||
.factory(OgnlGuard.class, StrutsOgnlGuard.class, Scope.SINGLETON)
|
||||
@@ -601,26 +612,39 @@ 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) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (actions == null) {
|
||||
return null;
|
||||
}
|
||||
return config;
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
* 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,28 +20,65 @@ package org.apache.struts2.dispatcher;
|
||||
|
||||
import org.apache.struts2.inject.Container;
|
||||
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
|
||||
/**
|
||||
* Simple class to hold Container instance per thread to minimise number of attempts
|
||||
* to read configuration and build each time a new configuration.
|
||||
* Per-thread cache for the Container instance, minimising repeated reads from
|
||||
* {@link org.apache.struts2.config.ConfigurationManager}.
|
||||
* <p>
|
||||
* 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.
|
||||
* 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.
|
||||
*/
|
||||
class ContainerHolder {
|
||||
|
||||
private static final ThreadLocal<Container> instance = new ThreadLocal<>();
|
||||
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();
|
||||
|
||||
public static void store(Container newInstance) {
|
||||
instance.set(newInstance);
|
||||
instance.set(new CachedContainer(newInstance, generation.get()));
|
||||
}
|
||||
|
||||
public static Container get() {
|
||||
return instance.get();
|
||||
CachedContainer cached = instance.get();
|
||||
if (cached == null) {
|
||||
return null;
|
||||
}
|
||||
if (cached.generation() != generation.get()) {
|
||||
instance.remove();
|
||||
return null;
|
||||
}
|
||||
return cached.container();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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) {}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
/*
|
||||
* 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).
|
||||
*
|
||||
* <p>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()}.</p>
|
||||
*
|
||||
* @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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
* 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,37 +441,78 @@ public class Dispatcher {
|
||||
* Releases all instances bound to this dispatcher instance.
|
||||
*/
|
||||
public void cleanup() {
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
destroyObjectFactory();
|
||||
|
||||
// clean up Dispatcher itself for this thread
|
||||
instance.remove();
|
||||
servletContext.setAttribute(StrutsStatics.SERVLET_DISPATCHER, null);
|
||||
|
||||
// clean up DispatcherListeners
|
||||
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() {
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
// clean up all interceptors by calling their destroy() method
|
||||
/**
|
||||
* Destroys all interceptors registered in the current configuration.
|
||||
*
|
||||
* @since 7.2.0
|
||||
*/
|
||||
protected void destroyInterceptors() {
|
||||
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) {
|
||||
for (InterceptorMapping interceptorMapping : ((InterceptorStackConfig) config).getInterceptors()) {
|
||||
if (config instanceof InterceptorStackConfig isc) {
|
||||
for (InterceptorMapping interceptorMapping : isc.getInterceptors()) {
|
||||
interceptors.add(interceptorMapping.getInterceptor());
|
||||
}
|
||||
}
|
||||
@@ -480,16 +521,38 @@ public class Dispatcher {
|
||||
for (Interceptor interceptor : interceptors) {
|
||||
interceptor.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
// Clear container holder when application is unloaded / server shutdown
|
||||
ContainerHolder.clear();
|
||||
|
||||
//cleanup action context
|
||||
ActionContext.clear();
|
||||
|
||||
// clean up configuration
|
||||
configurationManager.destroyConfiguration();
|
||||
configurationManager = null;
|
||||
/**
|
||||
* 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");
|
||||
}
|
||||
}
|
||||
|
||||
private void init_FileManager() throws ClassNotFoundException {
|
||||
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
* 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();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
/*
|
||||
* 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");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
/*
|
||||
* 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.
|
||||
*
|
||||
* <p>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.</p>
|
||||
*
|
||||
* <p>This is not part of the public user API. For user/plugin lifecycle
|
||||
* callbacks, use {@link DispatcherListener} instead.</p>
|
||||
*
|
||||
* @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();
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.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,6 +77,7 @@ public class PrepareOperations {
|
||||
} finally {
|
||||
ActionContext.clear();
|
||||
Dispatcher.clearInstance();
|
||||
ContainerHolder.clear();
|
||||
devModeOverride.remove();
|
||||
}
|
||||
});
|
||||
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
* 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();
|
||||
}
|
||||
}
|
||||
+3
-3
@@ -215,15 +215,15 @@ public abstract class AbstractMultiPartRequest implements MultiPartRequest {
|
||||
|
||||
if (maxSize != null) {
|
||||
LOG.debug("Applies max size: {} to file upload request", maxSize);
|
||||
servletFileUpload.setSizeMax(maxSize);
|
||||
servletFileUpload.setMaxSize(maxSize);
|
||||
}
|
||||
if (maxFiles != null) {
|
||||
LOG.debug("Applies max files number: {} to file upload request", maxFiles);
|
||||
servletFileUpload.setFileCountMax(maxFiles);
|
||||
servletFileUpload.setMaxFileCount(maxFiles);
|
||||
}
|
||||
if (maxFileSize != null) {
|
||||
LOG.debug("Applies max size of single file: {} to file upload request", maxFileSize);
|
||||
servletFileUpload.setFileSizeMax(maxFileSize);
|
||||
servletFileUpload.setMaxFileSize(maxFileSize);
|
||||
}
|
||||
return servletFileUpload;
|
||||
}
|
||||
|
||||
@@ -130,4 +130,13 @@ 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,6 +651,25 @@ 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,6 +18,7 @@ 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;
|
||||
|
||||
@@ -26,11 +27,13 @@ import java.util.logging.Logger;
|
||||
*
|
||||
* @author Bob Lee (crazybob@google.com)
|
||||
*/
|
||||
class FinalizableReferenceQueue extends ReferenceQueue<Object> {
|
||||
public 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) {
|
||||
@@ -49,18 +52,39 @@ class FinalizableReferenceQueue extends ReferenceQueue<Object> {
|
||||
Thread thread = new Thread("FinalizableReferenceQueue") {
|
||||
@Override
|
||||
public void run() {
|
||||
while (true) {
|
||||
while (!Thread.currentThread().isInterrupted()) {
|
||||
try {
|
||||
cleanUp(remove());
|
||||
} catch (InterruptedException e) { /* ignore */ }
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
thread.setDaemon(true);
|
||||
thread.start();
|
||||
cleanupThread.set(thread);
|
||||
}
|
||||
|
||||
static ReferenceQueue<Object> instance = createAndStart();
|
||||
/**
|
||||
* 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 FinalizableReferenceQueue createAndStart() {
|
||||
FinalizableReferenceQueue queue = new FinalizableReferenceQueue();
|
||||
@@ -72,6 +96,17 @@ class FinalizableReferenceQueue extends ReferenceQueue<Object> {
|
||||
* Gets instance.
|
||||
*/
|
||||
public static ReferenceQueue<Object> getInstance() {
|
||||
return instance;
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ import org.apache.struts2.inject.Inject;
|
||||
import org.apache.struts2.result.ActionChainResult;
|
||||
import org.apache.struts2.result.Result;
|
||||
import org.apache.struts2.util.CompoundRoot;
|
||||
import org.apache.struts2.util.ProxyUtil;
|
||||
import org.apache.struts2.util.ProxyService;
|
||||
import org.apache.struts2.util.TextParseUtil;
|
||||
import org.apache.struts2.util.ValueStack;
|
||||
import org.apache.struts2.util.reflection.ReflectionProvider;
|
||||
@@ -96,7 +96,7 @@ import java.util.Map;
|
||||
* </p>
|
||||
* <!-- END SNIPPET: extending -->
|
||||
* <u>Example code:</u>
|
||||
*
|
||||
* <p>
|
||||
* <!-- START SNIPPET: example -->
|
||||
* <pre>
|
||||
* <action name="someAction" class="com.examples.SomeAction">
|
||||
@@ -114,7 +114,6 @@ import java.util.Map;
|
||||
* </pre>
|
||||
* <!-- END SNIPPET: example -->
|
||||
*
|
||||
*
|
||||
* @author mrdon
|
||||
* @author tm_jee ( tm_jee(at)yahoo.co.uk )
|
||||
* @see ActionChainResult
|
||||
@@ -135,12 +134,18 @@ public class ChainingInterceptor extends AbstractInterceptor {
|
||||
|
||||
protected Collection<String> includes;
|
||||
protected ReflectionProvider reflectionProvider;
|
||||
private ProxyService proxyService;
|
||||
|
||||
@Inject
|
||||
public void setReflectionProvider(ReflectionProvider prov) {
|
||||
this.reflectionProvider = prov;
|
||||
}
|
||||
|
||||
@Inject
|
||||
public void setProxyService(ProxyService proxyService) {
|
||||
this.proxyService = proxyService;
|
||||
}
|
||||
|
||||
@Inject(value = StrutsConstants.STRUTS_CHAINING_COPY_ERRORS, required = false)
|
||||
public void setCopyErrors(String copyErrors) {
|
||||
this.copyErrors = "true".equalsIgnoreCase(copyErrors);
|
||||
@@ -175,8 +180,8 @@ public class ChainingInterceptor extends AbstractInterceptor {
|
||||
}
|
||||
Object action = invocation.getAction();
|
||||
Class<?> editable = null;
|
||||
if (ProxyUtil.isProxy(action)) {
|
||||
editable = ProxyUtil.ultimateTargetClass(action);
|
||||
if (proxyService.isProxy(action)) {
|
||||
editable = proxyService.ultimateTargetClass(action);
|
||||
}
|
||||
reflectionProvider.copy(object, action, ctxMap, prepareExcludes(), includes, editable);
|
||||
}
|
||||
@@ -184,7 +189,7 @@ public class ChainingInterceptor extends AbstractInterceptor {
|
||||
|
||||
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,6 +19,8 @@
|
||||
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;
|
||||
@@ -39,24 +41,27 @@ 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);
|
||||
|
||||
@@ -68,8 +73,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("__checkbox_")) {
|
||||
String checkboxName = name.substring("__checkbox_".length());
|
||||
if (name.startsWith(hiddenPrefix)) {
|
||||
String checkboxName = name.substring(hiddenPrefix.length());
|
||||
|
||||
Parameter value = parameter.getValue();
|
||||
checkboxParameters.add(name);
|
||||
@@ -100,4 +105,16 @@ 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,18 +26,11 @@ 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;
|
||||
|
||||
@@ -108,6 +101,10 @@ 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;
|
||||
@@ -219,202 +216,167 @@ public class I18nInterceptor extends AbstractInterceptor {
|
||||
/**
|
||||
* Uses to handle reading/storing Locale from/in different locations
|
||||
*/
|
||||
protected interface LocaleHandler {
|
||||
Locale find();
|
||||
Locale read(ActionInvocation invocation);
|
||||
Locale store(ActionInvocation invocation, Locale locale);
|
||||
boolean shouldStore();
|
||||
@Deprecated(forRemoval = true, since = "7.2.0")
|
||||
protected interface LocaleHandler extends org.apache.struts2.interceptor.i18n.LocaleHandler {
|
||||
}
|
||||
|
||||
protected class RequestLocaleHandler implements LocaleHandler {
|
||||
/**
|
||||
* @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 ActionInvocation actionInvocation;
|
||||
protected boolean shouldStore = true;
|
||||
private final org.apache.struts2.interceptor.i18n.LocaleHandler delegate;
|
||||
|
||||
protected RequestLocaleHandler(ActionInvocation invocation) {
|
||||
actionInvocation = invocation;
|
||||
}
|
||||
|
||||
public Locale find() {
|
||||
LOG.debug("Searching locale in request under parameter {}", requestOnlyParameterName);
|
||||
|
||||
Parameter requestedLocale = findLocaleParameter(actionInvocation, requestOnlyParameterName);
|
||||
if (requestedLocale.isDefined()) {
|
||||
return getLocaleFromParam(requestedLocale.getValue());
|
||||
}
|
||||
|
||||
return null;
|
||||
protected LocaleHandlerAdapter(org.apache.struts2.interceptor.i18n.LocaleHandler delegate) {
|
||||
this.delegate = delegate;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Locale store(ActionInvocation invocation, Locale locale) {
|
||||
return locale;
|
||||
public Locale find() {
|
||||
return delegate.find();
|
||||
}
|
||||
|
||||
@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;
|
||||
return delegate.read(invocation);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Locale store(ActionInvocation invocation, Locale locale) {
|
||||
return delegate.store(invocation, locale);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean shouldStore() {
|
||||
return shouldStore;
|
||||
return delegate.shouldStore();
|
||||
}
|
||||
}
|
||||
|
||||
protected class AcceptLanguageLocaleHandler extends RequestLocaleHandler {
|
||||
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);
|
||||
}
|
||||
|
||||
@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(invocation);
|
||||
super(createAcceptLanguageDelegate(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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
protected class SessionLocaleHandler extends AcceptLanguageLocaleHandler {
|
||||
|
||||
/**
|
||||
* @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 SessionLocaleHandler(ActionInvocation 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;
|
||||
super(createSessionDelegate(invocation));
|
||||
}
|
||||
}
|
||||
|
||||
protected class CookieLocaleHandler extends AcceptLanguageLocaleHandler {
|
||||
/**
|
||||
* @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 CookieLocaleHandler(ActionInvocation 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;
|
||||
super(createCookieDelegate(invocation));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -263,6 +263,15 @@ 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) {
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
/*
|
||||
* 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);
|
||||
}
|
||||
+81
@@ -0,0 +1,81 @@
|
||||
/*
|
||||
* 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
+78
@@ -0,0 +1,78 @@
|
||||
/*
|
||||
* 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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
/*
|
||||
* 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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
/*
|
||||
* 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();
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
/*
|
||||
* 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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
/*
|
||||
* 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);
|
||||
}
|
||||
}
|
||||
+9
-3
@@ -39,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.ProxyUtil;
|
||||
import org.apache.struts2.util.ProxyService;
|
||||
import org.apache.struts2.util.TextParseUtil;
|
||||
import org.apache.struts2.util.ValueStack;
|
||||
import org.apache.struts2.util.ValueStackFactory;
|
||||
@@ -95,6 +95,7 @@ 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;
|
||||
@@ -115,6 +116,11 @@ public class ParametersInterceptor extends MethodFilterInterceptor {
|
||||
this.threadAllowlist = threadAllowlist;
|
||||
}
|
||||
|
||||
@Inject
|
||||
public void setProxyService(ProxyService proxyService) {
|
||||
this.proxyService = proxyService;
|
||||
}
|
||||
|
||||
@Inject(StrutsConstants.STRUTS_DEVMODE)
|
||||
public void setDevMode(String mode) {
|
||||
this.devMode = BooleanUtils.toBoolean(mode);
|
||||
@@ -516,8 +522,8 @@ public class ParametersInterceptor extends MethodFilterInterceptor {
|
||||
}
|
||||
|
||||
protected Class<?> ultimateClass(Object action) {
|
||||
if (ProxyUtil.isProxy(action)) {
|
||||
return ProxyUtil.ultimateTargetClass(action);
|
||||
if (proxyService.isProxy(action)) {
|
||||
return proxyService.ultimateTargetClass(action);
|
||||
}
|
||||
return action.getClass();
|
||||
}
|
||||
|
||||
@@ -56,4 +56,9 @@ public class MockContainer implements Container {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void destroy() {
|
||||
// no-op in mock
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -18,10 +18,9 @@
|
||||
*/
|
||||
package org.apache.struts2.ognl;
|
||||
|
||||
import ognl.OgnlContext;
|
||||
import org.apache.struts2.conversion.NullHandler;
|
||||
|
||||
public class OgnlNullHandlerWrapper implements ognl.NullHandler {
|
||||
public class OgnlNullHandlerWrapper implements ognl.NullHandler<StrutsContext> {
|
||||
|
||||
private final NullHandler wrapped;
|
||||
|
||||
@@ -30,13 +29,13 @@ public class OgnlNullHandlerWrapper implements ognl.NullHandler {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object nullMethodResult(OgnlContext context, Object target,
|
||||
public Object nullMethodResult(StrutsContext context, Object target,
|
||||
String methodName, Object[] args) {
|
||||
return wrapped.nullMethodResult(context, target, methodName, args);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object nullPropertyValue(OgnlContext context, Object target, Object property) {
|
||||
public Object nullPropertyValue(StrutsContext context, Object target, Object property) {
|
||||
return wrapped.nullPropertyValue(context, target, property);
|
||||
}
|
||||
|
||||
|
||||
@@ -29,6 +29,7 @@ import ognl.Ognl;
|
||||
public class OgnlReflectionContextFactory implements ReflectionContextFactory {
|
||||
|
||||
@Override
|
||||
@SuppressWarnings({"rawtypes", "unchecked"})
|
||||
public OgnlContext createDefaultContext(Object root) {
|
||||
return Ognl.createDefaultContext(root);
|
||||
}
|
||||
|
||||
@@ -18,7 +18,6 @@
|
||||
*/
|
||||
package org.apache.struts2.ognl;
|
||||
|
||||
import ognl.OgnlContext;
|
||||
import org.apache.struts2.conversion.TypeConverter;
|
||||
|
||||
import java.lang.reflect.Member;
|
||||
@@ -26,7 +25,7 @@ import java.lang.reflect.Member;
|
||||
/**
|
||||
* Wraps an XWork type conversion class for as an OGNL TypeConverter
|
||||
*/
|
||||
public class OgnlTypeConverterWrapper implements ognl.TypeConverter {
|
||||
public class OgnlTypeConverterWrapper implements ognl.TypeConverter<StrutsContext> {
|
||||
|
||||
private final TypeConverter typeConverter;
|
||||
|
||||
@@ -38,7 +37,7 @@ public class OgnlTypeConverterWrapper implements ognl.TypeConverter {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object convertValue(OgnlContext context, Object target, Member member, String propertyName, Object value, Class<?> toType) {
|
||||
public Object convertValue(StrutsContext context, Object target, Member member, String propertyName, Object value, Class<?> toType) {
|
||||
return typeConverter.convertValue(context, target, member, propertyName, value, toType);
|
||||
}
|
||||
|
||||
|
||||
@@ -61,7 +61,7 @@ public class OgnlUtil {
|
||||
|
||||
private final OgnlCache<String, Object> expressionCache;
|
||||
private final OgnlCache<Class<?>, BeanInfo> beanInfoCache;
|
||||
private TypeConverter defaultConverter;
|
||||
private TypeConverter<StrutsContext> defaultConverter;
|
||||
private final OgnlGuard ognlGuard;
|
||||
|
||||
private boolean devMode;
|
||||
@@ -211,14 +211,14 @@ public class OgnlUtil {
|
||||
* @return an OgnlContext instance
|
||||
* @since 7.2.0
|
||||
*/
|
||||
private OgnlContext ensureOgnlContext(Map<String, Object> context) {
|
||||
if (context instanceof OgnlContext ognlContext) {
|
||||
return ognlContext;
|
||||
private StrutsContext ensureOgnlContext(Map<String, Object> context) {
|
||||
if (context instanceof StrutsContext strutsContext) {
|
||||
return strutsContext;
|
||||
}
|
||||
// Create a new OgnlContext and copy the Map contents
|
||||
OgnlContext ognlContext = createDefaultContext(null);
|
||||
ognlContext.putAll(context);
|
||||
return ognlContext;
|
||||
// Create a new StrutsContext and copy the Map contents
|
||||
StrutsContext strutsContext = createDefaultContext(null);
|
||||
strutsContext.putAll(context);
|
||||
return strutsContext;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -247,16 +247,18 @@ public class OgnlUtil {
|
||||
return;
|
||||
}
|
||||
|
||||
OgnlContext ognlContext = ensureOgnlContext(context);
|
||||
Object oldRoot = Ognl.getRoot(ognlContext);
|
||||
Ognl.setRoot(ognlContext, o);
|
||||
|
||||
for (Map.Entry<String, ?> entry : props.entrySet()) {
|
||||
String expression = entry.getKey();
|
||||
internalSetProperty(expression, entry.getValue(), o, context, throwPropertyExceptions);
|
||||
StrutsContext strutsContext = ensureOgnlContext(context);
|
||||
try {
|
||||
withRoot(strutsContext, o, () -> {
|
||||
for (Map.Entry<String, ?> entry : props.entrySet()) {
|
||||
String expression = entry.getKey();
|
||||
internalSetProperty(expression, entry.getValue(), o, context, throwPropertyExceptions);
|
||||
}
|
||||
});
|
||||
} catch (OgnlException e) {
|
||||
// Should never happen as internalSetProperty catches OgnlException
|
||||
throw new IllegalStateException("Unexpected OgnlException in setProperties", e);
|
||||
}
|
||||
|
||||
Ognl.setRoot(ognlContext, oldRoot);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -307,14 +309,13 @@ public class OgnlUtil {
|
||||
* problems setting the property
|
||||
*/
|
||||
public void setProperty(String name, Object value, Object o, Map<String, Object> context, boolean throwPropertyExceptions) {
|
||||
|
||||
OgnlContext ognlContext = ensureOgnlContext(context);
|
||||
Object oldRoot = Ognl.getRoot(ognlContext);
|
||||
Ognl.setRoot(ognlContext, o);
|
||||
|
||||
internalSetProperty(name, value, o, context, throwPropertyExceptions);
|
||||
|
||||
Ognl.setRoot(ognlContext, oldRoot);
|
||||
StrutsContext strutsContext = ensureOgnlContext(context);
|
||||
try {
|
||||
withRoot(strutsContext, o, () -> internalSetProperty(name, value, o, context, throwPropertyExceptions));
|
||||
} catch (OgnlException e) {
|
||||
// Should never happen as internalSetProperty catches OgnlException
|
||||
throw new IllegalStateException("Unexpected OgnlException in setProperty", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -423,15 +424,18 @@ public class OgnlUtil {
|
||||
for (TreeValidator validator : treeValidators) {
|
||||
validator.validate(tree, checkContext);
|
||||
}
|
||||
Ognl.setValue(tree, (OgnlContext) context, root, value);
|
||||
StrutsContext ognlContext = (StrutsContext) context;
|
||||
withRoot(ognlContext, root, () -> Ognl.setValue(tree, ognlContext, root, value));
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private <T> T ognlGet(String expr, Map<String, Object> context, Object root, Class<T> resultType, Map<String, Object> checkContext, TreeValidator... treeValidators) throws OgnlException {
|
||||
Object tree = toTree(expr);
|
||||
for (TreeValidator validator : treeValidators) {
|
||||
validator.validate(tree, checkContext);
|
||||
}
|
||||
return (T) Ognl.getValue(tree, (OgnlContext) context, root, resultType);
|
||||
StrutsContext ognlContext = (StrutsContext) context;
|
||||
return withRoot(ognlContext, root, () -> (T) Ognl.getValue(tree, ognlContext, root, resultType));
|
||||
}
|
||||
|
||||
private Object toTree(String expr) throws OgnlException {
|
||||
@@ -544,8 +548,8 @@ public class OgnlUtil {
|
||||
return;
|
||||
}
|
||||
|
||||
final Map<String, Object> contextFrom = createDefaultContext(from);
|
||||
final Map<String, Object> contextTo = createDefaultContext(to);
|
||||
final StrutsContext contextFrom = createDefaultContext(from);
|
||||
final StrutsContext contextTo = createDefaultContext(to);
|
||||
|
||||
PropertyDescriptor[] fromPds;
|
||||
PropertyDescriptor[] toPds;
|
||||
@@ -650,7 +654,7 @@ public class OgnlUtil {
|
||||
*/
|
||||
public Map<String, Object> getBeanMap(final Object source) throws IntrospectionException, OgnlException {
|
||||
Map<String, Object> beanMap = new HashMap<>();
|
||||
final Map<String, Object> sourceMap = createDefaultContext(source);
|
||||
final StrutsContext sourceMap = createDefaultContext(source);
|
||||
PropertyDescriptor[] propertyDescriptors = getPropertyDescriptors(source);
|
||||
for (PropertyDescriptor propertyDescriptor : propertyDescriptors) {
|
||||
final String propertyName = propertyDescriptor.getDisplayName();
|
||||
@@ -720,22 +724,75 @@ public class OgnlUtil {
|
||||
}
|
||||
}
|
||||
|
||||
protected OgnlContext createDefaultContext(Object root) {
|
||||
protected StrutsContext createDefaultContext(Object root) {
|
||||
return createDefaultContext(root, null);
|
||||
}
|
||||
|
||||
protected OgnlContext createDefaultContext(Object root, ClassResolver resolver) {
|
||||
protected StrutsContext createDefaultContext(Object root, ClassResolver<StrutsContext> resolver) {
|
||||
if (resolver == null) {
|
||||
resolver = container.getInstance(RootAccessor.class);
|
||||
if (resolver == null) {
|
||||
throw new IllegalStateException("Cannot find ClassResolver");
|
||||
}
|
||||
}
|
||||
return Ognl.createDefaultContext(root, container.getInstance(SecurityMemberAccess.class), resolver, defaultConverter);
|
||||
StrutsContext context = new StrutsContext(
|
||||
container.getInstance(SecurityMemberAccess.class), resolver, defaultConverter);
|
||||
context.withRoot(root);
|
||||
return context;
|
||||
}
|
||||
|
||||
@FunctionalInterface
|
||||
private interface TreeValidator {
|
||||
void validate(Object tree, Map<String, Object> context) throws OgnlException;
|
||||
}
|
||||
|
||||
@FunctionalInterface
|
||||
private interface OgnlAction {
|
||||
void run() throws OgnlException;
|
||||
}
|
||||
|
||||
@FunctionalInterface
|
||||
private interface OgnlSupplier<T> {
|
||||
T get() throws OgnlException;
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes the given action with the specified root set on the OGNL context,
|
||||
* restoring the original root afterwards.
|
||||
*
|
||||
* @param context the OGNL context
|
||||
* @param root the root object to set during execution
|
||||
* @param action the action to execute
|
||||
* @throws OgnlException if the action throws an OgnlException
|
||||
*/
|
||||
private void withRoot(StrutsContext context, Object root, OgnlAction action) throws OgnlException {
|
||||
Object oldRoot = context.getRoot();
|
||||
try {
|
||||
context.withRoot(root);
|
||||
action.run();
|
||||
} finally {
|
||||
context.withRoot(oldRoot);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes the given supplier with the specified root set on the OGNL context,
|
||||
* restoring the original root afterwards.
|
||||
*
|
||||
* @param context the OGNL context
|
||||
* @param root the root object to set during execution
|
||||
* @param supplier the supplier to execute
|
||||
* @param <T> the return type
|
||||
* @return the result of the supplier
|
||||
* @throws OgnlException if the supplier throws an OgnlException
|
||||
*/
|
||||
private <T> T withRoot(StrutsContext context, Object root, OgnlSupplier<T> supplier) throws OgnlException {
|
||||
Object oldRoot = context.getRoot();
|
||||
try {
|
||||
context.withRoot(root);
|
||||
return supplier.get();
|
||||
} finally {
|
||||
context.withRoot(oldRoot);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,8 +31,6 @@ import org.apache.struts2.util.ValueStack;
|
||||
import org.apache.struts2.util.reflection.ReflectionContextState;
|
||||
import ognl.MethodFailedException;
|
||||
import ognl.NoSuchPropertyException;
|
||||
import ognl.Ognl;
|
||||
import ognl.OgnlContext;
|
||||
import ognl.OgnlException;
|
||||
import org.apache.commons.lang3.BooleanUtils;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
@@ -68,7 +66,7 @@ public class OgnlValueStack implements Serializable, ValueStack, ClearableValueS
|
||||
private static final String MAP_IDENTIFIER_KEY = "org.apache.struts2.util.OgnlValueStack.MAP_IDENTIFIER_KEY";
|
||||
|
||||
protected CompoundRoot root;
|
||||
protected transient Map<String, Object> context;
|
||||
protected transient StrutsContext context;
|
||||
protected Class defaultType;
|
||||
protected Map<Object, Object> overrides;
|
||||
protected transient OgnlUtil ognlUtil;
|
||||
@@ -121,12 +119,12 @@ public class OgnlValueStack implements Serializable, ValueStack, ClearableValueS
|
||||
protected void setRoot(XWorkConverter xworkConverter, RootAccessor accessor, CompoundRoot compoundRoot, SecurityMemberAccess securityMemberAccess) {
|
||||
this.root = compoundRoot;
|
||||
this.securityMemberAccess = securityMemberAccess;
|
||||
OgnlContext ognlContext = Ognl.createDefaultContext(this.root, securityMemberAccess, accessor, new OgnlTypeConverterWrapper(xworkConverter));
|
||||
this.context = ognlContext;
|
||||
this.context = new StrutsContext(securityMemberAccess, accessor, new OgnlTypeConverterWrapper(xworkConverter));
|
||||
this.context.withRoot(this.root);
|
||||
this.converter = xworkConverter;
|
||||
context.put(VALUE_STACK, this);
|
||||
ognlContext.setTraceEvaluations(false);
|
||||
ognlContext.setKeepLastEvaluation(false);
|
||||
context.setTraceEvaluations(false);
|
||||
context.setKeepLastEvaluation(false);
|
||||
}
|
||||
|
||||
@Inject(StrutsConstants.STRUTS_DEVMODE)
|
||||
@@ -508,9 +506,7 @@ public class OgnlValueStack implements Serializable, ValueStack, ClearableValueS
|
||||
|
||||
@Override
|
||||
public void clearContextValues() {
|
||||
//this is an OGNL ValueStack so the context will be an OgnlContext
|
||||
//it would be better to make context of type OgnlContext
|
||||
((OgnlContext) context).getValues().clear();
|
||||
context.getValues().clear();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -54,6 +54,7 @@ public class OgnlValueStackFactory implements ValueStackFactory {
|
||||
}
|
||||
|
||||
@Inject
|
||||
@SuppressWarnings({"rawtypes", "unchecked"})
|
||||
protected void setCompoundRootAccessor(RootAccessor compoundRootAccessor) {
|
||||
this.compoundRootAccessor = compoundRootAccessor;
|
||||
OgnlRuntime.setPropertyAccessor(CompoundRoot.class, compoundRootAccessor);
|
||||
@@ -110,6 +111,7 @@ public class OgnlValueStackFactory implements ValueStackFactory {
|
||||
* {@link #setMethodAccessor} and can be configured using the extension point
|
||||
* {@link StrutsConstants#STRUTS_METHOD_ACCESSOR}.
|
||||
*/
|
||||
@SuppressWarnings({"rawtypes", "unchecked"})
|
||||
protected void registerAdditionalMethodAccessors() {
|
||||
Set<String> names = container.getInstanceNames(MethodAccessor.class);
|
||||
for (String name : names) {
|
||||
@@ -145,6 +147,7 @@ public class OgnlValueStackFactory implements ValueStackFactory {
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings({"rawtypes", "unchecked"})
|
||||
protected void registerPropertyAccessors() throws ClassNotFoundException {
|
||||
Set<String> names = container.getInstanceNames(PropertyAccessor.class);
|
||||
for (String name : names) {
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
/*
|
||||
* Copyright 2022 Apache Software Foundation.
|
||||
*
|
||||
* Licensed 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.ognl;
|
||||
|
||||
/**
|
||||
* A proxy interface to be used with Struts DI mechanism for proxy detection caching.
|
||||
*
|
||||
* @param <Key> The type for the cache key entries
|
||||
* @param <Value> The type for the cache value entries
|
||||
* @since 7.2.0
|
||||
*/
|
||||
public interface ProxyCacheFactory<Key, Value> extends OgnlCacheFactory<Key, Value> {
|
||||
|
||||
}
|
||||
@@ -19,13 +19,12 @@
|
||||
package org.apache.struts2.ognl;
|
||||
|
||||
import ognl.MemberAccess;
|
||||
import ognl.OgnlContext;
|
||||
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.ProxyUtil;
|
||||
import org.apache.struts2.util.ProxyService;
|
||||
|
||||
import java.lang.reflect.AccessibleObject;
|
||||
import java.lang.reflect.Constructor;
|
||||
@@ -54,7 +53,7 @@ import static org.apache.struts2.util.DebugUtils.logWarningForFirstOccurrence;
|
||||
* Allows access decisions to be made on the basis of whether a member is static or not.
|
||||
* Also blocks or allows access to properties.
|
||||
*/
|
||||
public class SecurityMemberAccess implements MemberAccess {
|
||||
public class SecurityMemberAccess implements MemberAccess<StrutsContext> {
|
||||
|
||||
private static final Logger LOG = LogManager.getLogger(SecurityMemberAccess.class);
|
||||
|
||||
@@ -76,6 +75,8 @@ public class SecurityMemberAccess implements MemberAccess {
|
||||
private final ProviderAllowlist providerAllowlist;
|
||||
private final ThreadAllowlist threadAllowlist;
|
||||
|
||||
private ProxyService proxyService;
|
||||
|
||||
private boolean allowStaticFieldAccess = true;
|
||||
|
||||
private Set<Pattern> excludeProperties = emptySet();
|
||||
@@ -107,8 +108,13 @@ public class SecurityMemberAccess implements MemberAccess {
|
||||
this.threadAllowlist = threadAllowlist;
|
||||
}
|
||||
|
||||
@Inject
|
||||
public void setProxyService(ProxyService proxyService) {
|
||||
this.proxyService = proxyService;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object setup(OgnlContext context, Object target, Member member, String propertyName) {
|
||||
public Object setup(StrutsContext context, Object target, Member member, String propertyName) {
|
||||
Object result = null;
|
||||
|
||||
if (isAccessible(context, target, member, propertyName)) {
|
||||
@@ -123,7 +129,7 @@ public class SecurityMemberAccess implements MemberAccess {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void restore(OgnlContext context, Object target, Member member, String propertyName, Object state) {
|
||||
public void restore(StrutsContext context, Object target, Member member, String propertyName, Object state) {
|
||||
if (state == null) {
|
||||
return;
|
||||
}
|
||||
@@ -138,7 +144,7 @@ public class SecurityMemberAccess implements MemberAccess {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isAccessible(OgnlContext context, Object target, Member member, String propertyName) {
|
||||
public boolean isAccessible(StrutsContext context, Object target, Member member, String propertyName) {
|
||||
LOG.debug("Checking access for [target: {}, member: {}, property: {}]", target, member, propertyName);
|
||||
|
||||
if (member == null) {
|
||||
@@ -214,15 +220,15 @@ public class SecurityMemberAccess implements MemberAccess {
|
||||
|
||||
Class<?> targetClass = target != null ? target.getClass() : null;
|
||||
|
||||
if (!disallowProxyObjectAccess && ProxyUtil.isProxy(target)) {
|
||||
if (!disallowProxyObjectAccess && proxyService.isProxy(target)) {
|
||||
// If `disallowProxyObjectAccess` is not set, allow resolving Hibernate entities and Spring proxies to their
|
||||
// underlying classes/members. This allows the allowlist capability to continue working and still offer
|
||||
// protection in applications where the developer has accepted the risk of allowing OGNL access to Hibernate
|
||||
// entities and Spring proxies. This is preferred to having to disable the allowlist capability entirely.
|
||||
Class<?> newTargetClass = ProxyUtil.ultimateTargetClass(target);
|
||||
Class<?> newTargetClass = proxyService.ultimateTargetClass(target);
|
||||
if (newTargetClass != targetClass) {
|
||||
targetClass = newTargetClass;
|
||||
member = ProxyUtil.resolveTargetMember(member, newTargetClass);
|
||||
member = proxyService.resolveTargetMember(member, newTargetClass);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -312,14 +318,14 @@ public class SecurityMemberAccess implements MemberAccess {
|
||||
* @return {@code true} if proxy object access is allowed
|
||||
*/
|
||||
protected boolean checkProxyObjectAccess(Object target) {
|
||||
return !(disallowProxyObjectAccess && ProxyUtil.isProxy(target));
|
||||
return !(disallowProxyObjectAccess && proxyService.isProxy(target));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {@code true} if proxy member access is allowed
|
||||
*/
|
||||
protected boolean checkProxyMemberAccess(Object target, Member member) {
|
||||
return !(disallowProxyMemberAccess && ProxyUtil.isProxyMember(member, target));
|
||||
return !(disallowProxyMemberAccess && proxyService.isProxyMember(member, target));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
/*
|
||||
* 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.ognl;
|
||||
|
||||
import ognl.ClassResolver;
|
||||
import ognl.MemberAccess;
|
||||
import ognl.OgnlContext;
|
||||
import ognl.TypeConverter;
|
||||
|
||||
/**
|
||||
* Struts-specific OGNL evaluation context. Extends {@link OgnlContext} with the
|
||||
* self-bounded generic parameter to enable type-safe access in all OGNL interface
|
||||
* implementations ({@link MemberAccess}, {@link ognl.PropertyAccessor}, etc.).
|
||||
*
|
||||
* <p>Phase 1: minimal subclass delegating to super constructors.
|
||||
* Future phases will promote stringly-typed map entries (e.g. {@code DENY_METHOD_EXECUTION},
|
||||
* {@code CREATE_NULL_OBJECTS}) to proper typed fields.</p>
|
||||
*
|
||||
* @since 7.2.0
|
||||
*/
|
||||
public class StrutsContext extends OgnlContext<StrutsContext> {
|
||||
|
||||
public StrutsContext(MemberAccess<StrutsContext> memberAccess) {
|
||||
super(memberAccess);
|
||||
}
|
||||
|
||||
public StrutsContext(MemberAccess<StrutsContext> memberAccess,
|
||||
ClassResolver<StrutsContext> classResolver) {
|
||||
super(memberAccess, classResolver);
|
||||
}
|
||||
|
||||
public StrutsContext(MemberAccess<StrutsContext> memberAccess,
|
||||
ClassResolver<StrutsContext> classResolver,
|
||||
TypeConverter<StrutsContext> typeConverter) {
|
||||
super(memberAccess, classResolver, typeConverter);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
* Copyright 2022 Apache Software Foundation.
|
||||
*
|
||||
* Licensed 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.ognl;
|
||||
|
||||
import org.apache.commons.lang3.EnumUtils;
|
||||
import org.apache.struts2.StrutsConstants;
|
||||
import org.apache.struts2.inject.Inject;
|
||||
|
||||
/**
|
||||
* Struts proxy cache factory implementation.
|
||||
* Used for creating caches for proxy detection operations.
|
||||
*
|
||||
* @param <Key> The type for the cache key entries
|
||||
* @param <Value> The type for the cache value entries
|
||||
* @since 7.2.0
|
||||
*/
|
||||
public class StrutsProxyCacheFactory<Key, Value> extends DefaultOgnlCacheFactory<Key, Value>
|
||||
implements ProxyCacheFactory<Key, Value> {
|
||||
|
||||
@Inject
|
||||
public StrutsProxyCacheFactory(
|
||||
@Inject(value = StrutsConstants.STRUTS_PROXY_CACHE_MAXSIZE) String cacheMaxSize,
|
||||
@Inject(value = StrutsConstants.STRUTS_PROXY_CACHE_TYPE) String defaultCacheType) {
|
||||
super(Integer.parseInt(cacheMaxSize), EnumUtils.getEnumIgnoreCase(CacheType.class, defaultCacheType));
|
||||
}
|
||||
}
|
||||
@@ -18,7 +18,6 @@
|
||||
*/
|
||||
package org.apache.struts2.ognl;
|
||||
|
||||
import ognl.OgnlContext;
|
||||
import org.apache.struts2.conversion.TypeConverter;
|
||||
|
||||
import java.lang.reflect.Member;
|
||||
@@ -29,19 +28,19 @@ import java.util.Map;
|
||||
*/
|
||||
public class XWorkTypeConverterWrapper implements TypeConverter {
|
||||
|
||||
private final ognl.TypeConverter typeConverter;
|
||||
private final ognl.TypeConverter<StrutsContext> typeConverter;
|
||||
|
||||
public XWorkTypeConverterWrapper(ognl.TypeConverter conv) {
|
||||
public XWorkTypeConverterWrapper(ognl.TypeConverter<StrutsContext> conv) {
|
||||
this.typeConverter = conv;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object convertValue(Map context, Object target, Member member, String propertyName, Object value, Class toType) {
|
||||
// Cast context to OgnlContext for OGNL 3.4.8+ compatibility
|
||||
OgnlContext ognlContext = (context instanceof OgnlContext oc) ? oc : null;
|
||||
if (ognlContext == null) {
|
||||
throw new IllegalArgumentException("Context must be an OgnlContext for OGNL 3.4.8+");
|
||||
// Cast context to StrutsContext for OGNL 3.5.x compatibility
|
||||
StrutsContext strutsContext = (context instanceof StrutsContext sc) ? sc : null;
|
||||
if (strutsContext == null) {
|
||||
throw new IllegalArgumentException("Context must be a StrutsContext for OGNL 3.5.x+");
|
||||
}
|
||||
return typeConverter.convertValue(ognlContext, target, member, propertyName, value, toType);
|
||||
return typeConverter.convertValue(strutsContext, target, member, propertyName, value, toType);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
*/
|
||||
package org.apache.struts2.ognl.accessor;
|
||||
|
||||
import org.apache.struts2.dispatcher.InternalDestroyable;
|
||||
import org.apache.struts2.inject.Inject;
|
||||
import org.apache.struts2.ognl.OgnlValueStack;
|
||||
import org.apache.struts2.util.CompoundRoot;
|
||||
@@ -25,7 +26,6 @@ import org.apache.struts2.util.ValueStack;
|
||||
import ognl.MethodFailedException;
|
||||
import ognl.NoSuchPropertyException;
|
||||
import ognl.Ognl;
|
||||
import ognl.OgnlContext;
|
||||
import ognl.OgnlException;
|
||||
import ognl.OgnlRuntime;
|
||||
import org.apache.commons.lang3.BooleanUtils;
|
||||
@@ -33,6 +33,7 @@ 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.ognl.StrutsContext;
|
||||
|
||||
import java.beans.IntrospectionException;
|
||||
import java.beans.PropertyDescriptor;
|
||||
@@ -57,13 +58,13 @@ import static org.apache.commons.lang3.BooleanUtils.toBoolean;
|
||||
* @author Rainer Hermanns
|
||||
* @version $Revision$
|
||||
*/
|
||||
public class CompoundRootAccessor implements RootAccessor {
|
||||
public class CompoundRootAccessor implements RootAccessor, InternalDestroyable {
|
||||
|
||||
/**
|
||||
* Used by OGNl to generate bytecode
|
||||
*/
|
||||
@Override
|
||||
public String getSourceAccessor(OgnlContext context, Object target, Object index) {
|
||||
public String getSourceAccessor(StrutsContext context, Object target, Object index) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -71,7 +72,7 @@ public class CompoundRootAccessor implements RootAccessor {
|
||||
* Used by OGNl to generate bytecode
|
||||
*/
|
||||
@Override
|
||||
public String getSourceSetter(OgnlContext context, Object target, Object index) {
|
||||
public String getSourceSetter(StrutsContext context, Object target, Object index) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -95,7 +96,7 @@ public class CompoundRootAccessor implements RootAccessor {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setProperty(OgnlContext context, Object target, Object name, Object value) throws OgnlException {
|
||||
public void setProperty(StrutsContext context, Object target, Object name, Object value) throws OgnlException {
|
||||
CompoundRoot root = (CompoundRoot) target;
|
||||
|
||||
for (Object o : root) {
|
||||
@@ -137,7 +138,7 @@ public class CompoundRootAccessor implements RootAccessor {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getProperty(OgnlContext context, Object target, Object name) throws OgnlException {
|
||||
public Object getProperty(StrutsContext context, Object target, Object name) throws OgnlException {
|
||||
CompoundRoot root = (CompoundRoot) target;
|
||||
|
||||
if (name instanceof Integer index) {
|
||||
@@ -181,7 +182,7 @@ public class CompoundRootAccessor implements RootAccessor {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object callMethod(OgnlContext context, Object target, String name, Object[] objects) throws MethodFailedException {
|
||||
public Object callMethod(StrutsContext context, Object target, String name, Object[] objects) throws MethodFailedException {
|
||||
CompoundRoot root = (CompoundRoot) target;
|
||||
|
||||
if ("describe".equals(name)) {
|
||||
@@ -269,12 +270,12 @@ public class CompoundRootAccessor implements RootAccessor {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object callStaticMethod(OgnlContext transientVars, Class aClass, String s, Object[] objects) throws MethodFailedException {
|
||||
public Object callStaticMethod(StrutsContext transientVars, Class aClass, String s, Object[] objects) throws MethodFailedException {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class classForName(String className, OgnlContext context) throws ClassNotFoundException {
|
||||
public Class classForName(String className, StrutsContext context) throws ClassNotFoundException {
|
||||
Object root = Ognl.getRoot(context);
|
||||
|
||||
if (disallowCustomOgnlMap) {
|
||||
@@ -322,6 +323,18 @@ public class CompoundRootAccessor implements RootAccessor {
|
||||
}
|
||||
|
||||
|
||||
public static void clearCache() {
|
||||
invalidMethods.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* @since 7.2.0
|
||||
*/
|
||||
@Override
|
||||
public void destroy() {
|
||||
clearCache();
|
||||
}
|
||||
|
||||
static class MethodCall {
|
||||
Class clazz;
|
||||
String name;
|
||||
|
||||
+4
-4
@@ -19,20 +19,20 @@
|
||||
package org.apache.struts2.ognl.accessor;
|
||||
|
||||
import ognl.ObjectPropertyAccessor;
|
||||
import ognl.OgnlContext;
|
||||
import ognl.OgnlException;
|
||||
import org.apache.struts2.dispatcher.HttpParameters;
|
||||
import org.apache.struts2.ognl.StrutsContext;
|
||||
|
||||
public class HttpParametersPropertyAccessor extends ObjectPropertyAccessor {
|
||||
public class HttpParametersPropertyAccessor extends ObjectPropertyAccessor<StrutsContext> {
|
||||
|
||||
@Override
|
||||
public Object getProperty(OgnlContext context, Object target, Object oname) throws OgnlException {
|
||||
public Object getProperty(StrutsContext context, Object target, Object oname) throws OgnlException {
|
||||
HttpParameters parameters = (HttpParameters) target;
|
||||
return parameters.get(String.valueOf(oname)).getObject();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setProperty(OgnlContext context, Object target, Object oname, Object value) throws OgnlException {
|
||||
public void setProperty(StrutsContext context, Object target, Object oname, Object value) throws OgnlException {
|
||||
throw new OgnlException("Access to " + target.getClass().getName() + " is read-only!");
|
||||
}
|
||||
}
|
||||
@@ -21,12 +21,12 @@ package org.apache.struts2.ognl.accessor;
|
||||
import org.apache.struts2.conversion.impl.XWorkConverter;
|
||||
import org.apache.struts2.util.reflection.ReflectionContextState;
|
||||
import ognl.ObjectPropertyAccessor;
|
||||
import ognl.OgnlContext;
|
||||
import ognl.OgnlException;
|
||||
import org.apache.struts2.ognl.StrutsContext;
|
||||
|
||||
public class ObjectAccessor extends ObjectPropertyAccessor {
|
||||
public class ObjectAccessor extends ObjectPropertyAccessor<StrutsContext> {
|
||||
@Override
|
||||
public Object getProperty(OgnlContext map, Object o, Object o1) throws OgnlException {
|
||||
public Object getProperty(StrutsContext map, Object o, Object o1) throws OgnlException {
|
||||
Object obj = super.getProperty(map, o, o1);
|
||||
|
||||
map.put(XWorkConverter.LAST_BEAN_CLASS_ACCESSED, o.getClass());
|
||||
|
||||
+11
-9
@@ -20,10 +20,10 @@ package org.apache.struts2.ognl.accessor;
|
||||
|
||||
import org.apache.struts2.ognl.ObjectProxy;
|
||||
import org.apache.struts2.util.reflection.ReflectionContextState;
|
||||
import ognl.OgnlContext;
|
||||
import ognl.OgnlException;
|
||||
import ognl.OgnlRuntime;
|
||||
import ognl.PropertyAccessor;
|
||||
import org.apache.struts2.ognl.StrutsContext;
|
||||
|
||||
/**
|
||||
* Is able to access (set/get) properties on a given object.
|
||||
@@ -33,13 +33,13 @@ import ognl.PropertyAccessor;
|
||||
*
|
||||
* @author Gabe
|
||||
*/
|
||||
public class ObjectProxyPropertyAccessor implements PropertyAccessor {
|
||||
public class ObjectProxyPropertyAccessor implements PropertyAccessor<StrutsContext> {
|
||||
|
||||
/**
|
||||
* Used by OGNl to generate bytecode
|
||||
*/
|
||||
@Override
|
||||
public String getSourceAccessor(OgnlContext context, Object target, Object index) {
|
||||
public String getSourceAccessor(StrutsContext context, Object target, Object index) {
|
||||
return null; //To change body of implemented methods use File | Settings | File Templates.
|
||||
}
|
||||
|
||||
@@ -47,25 +47,27 @@ public class ObjectProxyPropertyAccessor implements PropertyAccessor {
|
||||
* Used by OGNl to generate bytecode
|
||||
*/
|
||||
@Override
|
||||
public String getSourceSetter(OgnlContext context, Object target, Object index) {
|
||||
public String getSourceSetter(StrutsContext context, Object target, Object index) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getProperty(OgnlContext context, Object target, Object name) throws OgnlException {
|
||||
@SuppressWarnings({"unchecked", "rawtypes"})
|
||||
public Object getProperty(StrutsContext context, Object target, Object name) throws OgnlException {
|
||||
ObjectProxy proxy = (ObjectProxy) target;
|
||||
setupContext(context, proxy);
|
||||
|
||||
return OgnlRuntime.getPropertyAccessor(proxy.getValue().getClass()).getProperty(context, target, name);
|
||||
return ((PropertyAccessor) OgnlRuntime.getPropertyAccessor(proxy.getValue().getClass())).getProperty(context, target, name);
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setProperty(OgnlContext context, Object target, Object name, Object value) throws OgnlException {
|
||||
@SuppressWarnings({"unchecked", "rawtypes"})
|
||||
public void setProperty(StrutsContext context, Object target, Object name, Object value) throws OgnlException {
|
||||
ObjectProxy proxy = (ObjectProxy) target;
|
||||
setupContext(context, proxy);
|
||||
|
||||
OgnlRuntime.getPropertyAccessor(proxy.getValue().getClass()).setProperty(context, target, name, value);
|
||||
((PropertyAccessor) OgnlRuntime.getPropertyAccessor(proxy.getValue().getClass())).setProperty(context, target, name, value);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -75,7 +77,7 @@ public class ObjectProxyPropertyAccessor implements PropertyAccessor {
|
||||
* @param context
|
||||
* @param proxy
|
||||
*/
|
||||
private void setupContext(OgnlContext context, ObjectProxy proxy) {
|
||||
private void setupContext(StrutsContext context, ObjectProxy proxy) {
|
||||
ReflectionContextState.setLastBeanClassAccessed(context, proxy.getLastClassAccessed());
|
||||
ReflectionContextState.setLastBeanPropertyAccessed(context, proxy.getLastPropertyAccessed());
|
||||
}
|
||||
|
||||
@@ -19,14 +19,14 @@
|
||||
package org.apache.struts2.ognl.accessor;
|
||||
|
||||
import ognl.ObjectPropertyAccessor;
|
||||
import ognl.OgnlContext;
|
||||
import ognl.OgnlException;
|
||||
import org.apache.struts2.dispatcher.Parameter;
|
||||
import org.apache.struts2.ognl.StrutsContext;
|
||||
|
||||
public class ParameterPropertyAccessor extends ObjectPropertyAccessor {
|
||||
public class ParameterPropertyAccessor extends ObjectPropertyAccessor<StrutsContext> {
|
||||
|
||||
@Override
|
||||
public Object getProperty(OgnlContext context, Object target, Object oname) throws OgnlException {
|
||||
public Object getProperty(StrutsContext context, Object target, Object oname) throws OgnlException {
|
||||
if (target instanceof Parameter parameter) {
|
||||
if ("value".equalsIgnoreCase(String.valueOf(oname))) {
|
||||
throw new OgnlException("Access to " + oname + " is not allowed! Call parameter name directly!");
|
||||
@@ -37,7 +37,7 @@ public class ParameterPropertyAccessor extends ObjectPropertyAccessor {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setProperty(OgnlContext context, Object target, Object oname, Object value) throws OgnlException {
|
||||
public void setProperty(StrutsContext context, Object target, Object oname, Object value) throws OgnlException {
|
||||
if (target instanceof Parameter) {
|
||||
throw new OgnlException("Access to " + target.getClass().getName() + " is read-only!");
|
||||
} else {
|
||||
|
||||
@@ -21,9 +21,10 @@ package org.apache.struts2.ognl.accessor;
|
||||
import ognl.ClassResolver;
|
||||
import ognl.MethodAccessor;
|
||||
import ognl.PropertyAccessor;
|
||||
import org.apache.struts2.ognl.StrutsContext;
|
||||
|
||||
/**
|
||||
* @since 6.4.0
|
||||
*/
|
||||
public interface RootAccessor extends PropertyAccessor, MethodAccessor, ClassResolver {
|
||||
public interface RootAccessor extends PropertyAccessor<StrutsContext>, MethodAccessor<StrutsContext>, ClassResolver<StrutsContext> {
|
||||
}
|
||||
|
||||
+7
-7
@@ -25,10 +25,10 @@ import org.apache.struts2.inject.Inject;
|
||||
import org.apache.struts2.ognl.OgnlUtil;
|
||||
import org.apache.struts2.util.reflection.ReflectionContextState;
|
||||
import ognl.ObjectPropertyAccessor;
|
||||
import ognl.OgnlContext;
|
||||
import ognl.OgnlException;
|
||||
import ognl.OgnlRuntime;
|
||||
import ognl.SetPropertyAccessor;
|
||||
import org.apache.struts2.ognl.StrutsContext;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
|
||||
@@ -40,7 +40,7 @@ import java.util.Map;
|
||||
/**
|
||||
* @author Gabe
|
||||
*/
|
||||
public class XWorkCollectionPropertyAccessor extends SetPropertyAccessor {
|
||||
public class XWorkCollectionPropertyAccessor extends SetPropertyAccessor<StrutsContext> {
|
||||
|
||||
private static final Logger LOG = LogManager.getLogger(XWorkCollectionPropertyAccessor.class);
|
||||
|
||||
@@ -87,7 +87,7 @@ public class XWorkCollectionPropertyAccessor extends SetPropertyAccessor {
|
||||
* @see ognl.PropertyAccessor#getProperty(java.util.Map, Object, Object)
|
||||
*/
|
||||
@Override
|
||||
public Object getProperty(OgnlContext context, Object target, Object key) throws OgnlException {
|
||||
public Object getProperty(StrutsContext context, Object target, Object key) throws OgnlException {
|
||||
LOG.trace("Entering getProperty()");
|
||||
|
||||
//check if it is a generic type property.
|
||||
@@ -186,7 +186,7 @@ public class XWorkCollectionPropertyAccessor extends SetPropertyAccessor {
|
||||
* Gets an indexed Map by a given key property with the key being
|
||||
* the value of the property and the value being the
|
||||
*/
|
||||
private Map getSetMap(OgnlContext context, Collection collection, String property) throws OgnlException {
|
||||
private Map getSetMap(StrutsContext context, Collection collection, String property) throws OgnlException {
|
||||
LOG.trace("getting set Map");
|
||||
|
||||
String path = ReflectionContextState.getCurrentPropertyPath(context);
|
||||
@@ -211,7 +211,7 @@ public class XWorkCollectionPropertyAccessor extends SetPropertyAccessor {
|
||||
/*
|
||||
* gets a bean with the given
|
||||
*/
|
||||
public Object getPropertyThroughIteration(OgnlContext context, Collection collection, String property, Object key)
|
||||
public Object getPropertyThroughIteration(StrutsContext context, Collection collection, String property, Object key)
|
||||
throws OgnlException {
|
||||
//TODO
|
||||
for (Object currTest : collection) {
|
||||
@@ -224,7 +224,7 @@ public class XWorkCollectionPropertyAccessor extends SetPropertyAccessor {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setProperty(OgnlContext context, Object target, Object name, Object value) throws OgnlException {
|
||||
public void setProperty(StrutsContext context, Object target, Object name, Object value) throws OgnlException {
|
||||
Class lastClass = (Class) context.get(XWorkConverter.LAST_BEAN_CLASS_ACCESSED);
|
||||
String lastProperty = (String) context.get(XWorkConverter.LAST_BEAN_PROPERTY_ACCESSED);
|
||||
Class convertToClass = objectTypeDeterminer.getElementClass(lastClass, lastProperty, name);
|
||||
@@ -256,7 +256,7 @@ public class XWorkCollectionPropertyAccessor extends SetPropertyAccessor {
|
||||
super.setProperty(context, target, name, realValue);
|
||||
}
|
||||
|
||||
private Object getRealValue(OgnlContext context, Object value, Class convertToClass) {
|
||||
private Object getRealValue(StrutsContext context, Object value, Class convertToClass) {
|
||||
if (value == null || convertToClass == null) {
|
||||
return value;
|
||||
}
|
||||
|
||||
@@ -20,15 +20,15 @@ package org.apache.struts2.ognl.accessor;
|
||||
|
||||
import ognl.EnumerationPropertyAccessor;
|
||||
import ognl.ObjectPropertyAccessor;
|
||||
import ognl.OgnlContext;
|
||||
import ognl.OgnlException;
|
||||
import org.apache.struts2.ognl.StrutsContext;
|
||||
|
||||
public class XWorkEnumerationAccessor extends EnumerationPropertyAccessor {
|
||||
public class XWorkEnumerationAccessor extends EnumerationPropertyAccessor<StrutsContext> {
|
||||
|
||||
private final ObjectPropertyAccessor opa = new ObjectPropertyAccessor();
|
||||
private final ObjectPropertyAccessor<StrutsContext> opa = new ObjectPropertyAccessor<>();
|
||||
|
||||
@Override
|
||||
public void setProperty(OgnlContext context, Object target, Object name, Object value) throws OgnlException {
|
||||
public void setProperty(StrutsContext context, Object target, Object name, Object value) throws OgnlException {
|
||||
opa.setProperty(context, target, name, value);
|
||||
}
|
||||
}
|
||||
|
||||
+4
-4
@@ -20,15 +20,15 @@ package org.apache.struts2.ognl.accessor;
|
||||
|
||||
import ognl.IteratorPropertyAccessor;
|
||||
import ognl.ObjectPropertyAccessor;
|
||||
import ognl.OgnlContext;
|
||||
import ognl.OgnlException;
|
||||
import org.apache.struts2.ognl.StrutsContext;
|
||||
|
||||
public class XWorkIteratorPropertyAccessor extends IteratorPropertyAccessor {
|
||||
public class XWorkIteratorPropertyAccessor extends IteratorPropertyAccessor<StrutsContext> {
|
||||
|
||||
private final ObjectPropertyAccessor opa = new ObjectPropertyAccessor();
|
||||
private final ObjectPropertyAccessor<StrutsContext> opa = new ObjectPropertyAccessor<>();
|
||||
|
||||
@Override
|
||||
public void setProperty(OgnlContext context, Object target, Object name, Object value) throws OgnlException {
|
||||
public void setProperty(StrutsContext context, Object target, Object name, Object value) throws OgnlException {
|
||||
opa.setProperty(context, target, name, value);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,9 +25,9 @@ import org.apache.struts2.inject.Inject;
|
||||
import org.apache.struts2.ognl.OgnlUtil;
|
||||
import org.apache.struts2.util.reflection.ReflectionContextState;
|
||||
import ognl.ListPropertyAccessor;
|
||||
import ognl.OgnlContext;
|
||||
import ognl.OgnlException;
|
||||
import ognl.PropertyAccessor;
|
||||
import org.apache.struts2.ognl.StrutsContext;
|
||||
import org.apache.struts2.StrutsConstants;
|
||||
import org.apache.struts2.StrutsException;
|
||||
|
||||
@@ -41,7 +41,7 @@ import java.util.List;
|
||||
*
|
||||
* @author Gabriel Zimmerman
|
||||
*/
|
||||
public class XWorkListPropertyAccessor extends ListPropertyAccessor {
|
||||
public class XWorkListPropertyAccessor extends ListPropertyAccessor<StrutsContext> {
|
||||
|
||||
private XWorkCollectionPropertyAccessor _sAcc = new XWorkCollectionPropertyAccessor();
|
||||
|
||||
@@ -57,7 +57,7 @@ public class XWorkListPropertyAccessor extends ListPropertyAccessor {
|
||||
}
|
||||
|
||||
@Inject("java.util.Collection")
|
||||
public void setXWorkCollectionPropertyAccessor(PropertyAccessor acc) {
|
||||
public void setXWorkCollectionPropertyAccessor(PropertyAccessor<StrutsContext> acc) {
|
||||
this._sAcc = (XWorkCollectionPropertyAccessor) acc;
|
||||
}
|
||||
|
||||
@@ -82,7 +82,7 @@ public class XWorkListPropertyAccessor extends ListPropertyAccessor {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getProperty(OgnlContext context, Object target, Object name) throws OgnlException {
|
||||
public Object getProperty(StrutsContext context, Object target, Object name) throws OgnlException {
|
||||
|
||||
if (ReflectionContextState.isGettingByKeyProperty(context)
|
||||
|| name.equals(XWorkCollectionPropertyAccessor.KEY_PROPERTY_FOR_CREATION)) {
|
||||
@@ -137,7 +137,7 @@ public class XWorkListPropertyAccessor extends ListPropertyAccessor {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setProperty(OgnlContext context, Object target, Object name, Object value)
|
||||
public void setProperty(StrutsContext context, Object target, Object name, Object value)
|
||||
throws OgnlException {
|
||||
|
||||
Class lastClass = (Class) context.get(XWorkConverter.LAST_BEAN_CLASS_ACCESSED);
|
||||
@@ -185,7 +185,7 @@ public class XWorkListPropertyAccessor extends ListPropertyAccessor {
|
||||
super.setProperty(context, target, name, realValue);
|
||||
}
|
||||
|
||||
private Object getRealValue(OgnlContext context, Object value, Class convertToClass) {
|
||||
private Object getRealValue(StrutsContext context, Object value, Class convertToClass) {
|
||||
if (value == null || convertToClass == null) {
|
||||
return value;
|
||||
}
|
||||
|
||||
@@ -24,8 +24,8 @@ import org.apache.struts2.conversion.impl.XWorkConverter;
|
||||
import org.apache.struts2.inject.Inject;
|
||||
import org.apache.struts2.util.reflection.ReflectionContextState;
|
||||
import ognl.MapPropertyAccessor;
|
||||
import ognl.OgnlContext;
|
||||
import ognl.OgnlException;
|
||||
import org.apache.struts2.ognl.StrutsContext;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
|
||||
@@ -37,7 +37,7 @@ import java.util.Map;
|
||||
*
|
||||
* @author Gabriel Zimmerman
|
||||
*/
|
||||
public class XWorkMapPropertyAccessor extends MapPropertyAccessor {
|
||||
public class XWorkMapPropertyAccessor extends MapPropertyAccessor<StrutsContext> {
|
||||
|
||||
private static final Logger LOG = LogManager.getLogger(XWorkMapPropertyAccessor.class);
|
||||
|
||||
@@ -63,7 +63,7 @@ public class XWorkMapPropertyAccessor extends MapPropertyAccessor {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getProperty(OgnlContext context, Object target, Object name) throws OgnlException {
|
||||
public Object getProperty(StrutsContext context, Object target, Object name) throws OgnlException {
|
||||
LOG.trace("Entering getProperty ({},{},{})", context, target, name);
|
||||
|
||||
ReflectionContextState.updateCurrentPropertyPath(context, name);
|
||||
@@ -123,7 +123,7 @@ public class XWorkMapPropertyAccessor extends MapPropertyAccessor {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setProperty(OgnlContext context, Object target, Object name, Object value) throws OgnlException {
|
||||
public void setProperty(StrutsContext context, Object target, Object name, Object value) throws OgnlException {
|
||||
LOG.trace("Entering setProperty({},{},{},{})", context, target, name, value);
|
||||
|
||||
Object key = getKey(context, name);
|
||||
@@ -131,7 +131,7 @@ public class XWorkMapPropertyAccessor extends MapPropertyAccessor {
|
||||
map.put(key, getValue(context, value));
|
||||
}
|
||||
|
||||
private Object getValue(OgnlContext context, Object value) {
|
||||
private Object getValue(StrutsContext context, Object value) {
|
||||
Class lastClass = (Class) context.get(XWorkConverter.LAST_BEAN_CLASS_ACCESSED);
|
||||
String lastProperty = (String) context.get(XWorkConverter.LAST_BEAN_PROPERTY_ACCESSED);
|
||||
if (lastClass == null || lastProperty == null) {
|
||||
@@ -144,7 +144,7 @@ public class XWorkMapPropertyAccessor extends MapPropertyAccessor {
|
||||
return xworkConverter.convertValue(context, value, elementClass);
|
||||
}
|
||||
|
||||
private Object getKey(OgnlContext context, Object name) {
|
||||
private Object getKey(StrutsContext context, Object name) {
|
||||
Class lastClass = (Class) context.get(XWorkConverter.LAST_BEAN_CLASS_ACCESSED);
|
||||
String lastProperty = (String) context.get(XWorkConverter.LAST_BEAN_PROPERTY_ACCESSED);
|
||||
if (lastClass == null || lastProperty == null) {
|
||||
|
||||
@@ -21,9 +21,9 @@ package org.apache.struts2.ognl.accessor;
|
||||
import org.apache.struts2.util.reflection.ReflectionContextState;
|
||||
import ognl.MethodFailedException;
|
||||
import ognl.ObjectMethodAccessor;
|
||||
import ognl.OgnlContext;
|
||||
import ognl.OgnlRuntime;
|
||||
import ognl.PropertyAccessor;
|
||||
import org.apache.struts2.ognl.StrutsContext;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
|
||||
@@ -38,12 +38,13 @@ import java.util.Collection;
|
||||
* @author Patrick Lightbody
|
||||
* @author tmjee
|
||||
*/
|
||||
public class XWorkMethodAccessor extends ObjectMethodAccessor {
|
||||
public class XWorkMethodAccessor extends ObjectMethodAccessor<StrutsContext> {
|
||||
|
||||
private static final Logger LOG = LogManager.getLogger(XWorkMethodAccessor.class);
|
||||
|
||||
@Override
|
||||
public Object callMethod(OgnlContext context, Object object, String string, Object[] objects) throws MethodFailedException {
|
||||
@SuppressWarnings("unchecked")
|
||||
public Object callMethod(StrutsContext context, Object object, String string, Object[] objects) throws MethodFailedException {
|
||||
|
||||
//Collection property accessing
|
||||
//this if statement ensures that ognl
|
||||
@@ -94,7 +95,7 @@ public class XWorkMethodAccessor extends ObjectMethodAccessor {
|
||||
}
|
||||
}
|
||||
|
||||
private Object callMethodWithDebugInfo(OgnlContext context, Object object, String methodName, Object[] objects) throws MethodFailedException {
|
||||
private Object callMethodWithDebugInfo(StrutsContext context, Object object, String methodName, Object[] objects) throws MethodFailedException {
|
||||
try {
|
||||
return super.callMethod(context, object, methodName, objects);
|
||||
} catch (MethodFailedException e) {
|
||||
@@ -109,7 +110,7 @@ public class XWorkMethodAccessor extends ObjectMethodAccessor {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object callStaticMethod(OgnlContext context, Class aClass, String string, Object[] objects) throws MethodFailedException {
|
||||
public Object callStaticMethod(StrutsContext context, Class aClass, String string, Object[] objects) throws MethodFailedException {
|
||||
boolean e = ReflectionContextState.isDenyMethodExecution(context);
|
||||
|
||||
if (!e) {
|
||||
@@ -119,7 +120,7 @@ public class XWorkMethodAccessor extends ObjectMethodAccessor {
|
||||
}
|
||||
}
|
||||
|
||||
private Object callStaticMethodWithDebugInfo(OgnlContext context, Class aClass, String methodName,
|
||||
private Object callStaticMethodWithDebugInfo(StrutsContext context, Class aClass, String methodName,
|
||||
Object[] objects) throws MethodFailedException {
|
||||
try {
|
||||
return super.callStaticMethod(context, aClass, methodName, objects);
|
||||
|
||||
+3
-3
@@ -21,15 +21,15 @@ package org.apache.struts2.ognl.accessor;
|
||||
import org.apache.struts2.conversion.impl.XWorkConverter;
|
||||
import org.apache.struts2.util.reflection.ReflectionContextState;
|
||||
import ognl.ObjectPropertyAccessor;
|
||||
import ognl.OgnlContext;
|
||||
import ognl.OgnlException;
|
||||
import org.apache.struts2.ognl.StrutsContext;
|
||||
|
||||
/**
|
||||
* @author Gabe
|
||||
*/
|
||||
public class XWorkObjectPropertyAccessor extends ObjectPropertyAccessor {
|
||||
public class XWorkObjectPropertyAccessor extends ObjectPropertyAccessor<StrutsContext> {
|
||||
@Override
|
||||
public Object getProperty(OgnlContext context, Object target, Object oname) throws OgnlException {
|
||||
public Object getProperty(StrutsContext context, Object target, Object oname) throws OgnlException {
|
||||
//set the last set objects in the context
|
||||
//so if the next objects accessed are
|
||||
//Maps or Collections they can use the information
|
||||
|
||||
@@ -32,6 +32,13 @@ public final class DebugUtils {
|
||||
|
||||
private static final Set<String> IS_LOGGED = ConcurrentHashMap.newKeySet();
|
||||
|
||||
/**
|
||||
* Clears the logged-keys cache to prevent memory leaks during hot redeployment.
|
||||
*/
|
||||
public static void clearCache() {
|
||||
IS_LOGGED.clear();
|
||||
}
|
||||
|
||||
public static void notifyDeveloperOfError(Logger log, Object action, String message) {
|
||||
if (action instanceof TextProvider tp) {
|
||||
message = tp.getText("devmode.notification", "Developer Notification:\n{0}", new String[]{message});
|
||||
|
||||
@@ -38,6 +38,7 @@ import org.xml.sax.SAXNotSupportedException;
|
||||
import org.xml.sax.SAXParseException;
|
||||
import org.xml.sax.helpers.DefaultHandler;
|
||||
|
||||
import javax.xml.XMLConstants;
|
||||
import javax.xml.parsers.ParserConfigurationException;
|
||||
import javax.xml.parsers.SAXParser;
|
||||
import javax.xml.parsers.SAXParserFactory;
|
||||
@@ -104,6 +105,7 @@ public class DomHelper {
|
||||
try {
|
||||
factory.setFeature("http://xml.org/sax/features/external-general-entities", false);
|
||||
factory.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
|
||||
factory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);
|
||||
} catch (ParserConfigurationException | SAXNotRecognizedException | SAXNotSupportedException e) {
|
||||
throw new StrutsException("Unable to disable resolving external entities!", e);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
/*
|
||||
* 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.util;
|
||||
|
||||
import java.lang.reflect.Member;
|
||||
|
||||
/**
|
||||
* Service interface for proxy detection and resolution operations.
|
||||
* Replaces static {@link ProxyUtil} methods with an injectable service.
|
||||
*
|
||||
* @since 7.2.0
|
||||
*/
|
||||
public interface ProxyService {
|
||||
|
||||
/**
|
||||
* Determine the ultimate target class of the given instance, traversing
|
||||
* not only a top-level proxy but any number of nested proxies as well &mdash;
|
||||
* as long as possible without side effects.
|
||||
*
|
||||
* @param candidate the instance to check (might be a proxy)
|
||||
* @return the ultimate target class (or the plain class of the given
|
||||
* object as fallback; never {@code null})
|
||||
*/
|
||||
Class<?> ultimateTargetClass(Object candidate);
|
||||
|
||||
/**
|
||||
* Check whether the given object is a proxy.
|
||||
*
|
||||
* @param object the object to check
|
||||
* @return true if the object is a Spring AOP or Hibernate proxy
|
||||
*/
|
||||
boolean isProxy(Object object);
|
||||
|
||||
/**
|
||||
* Check whether the given member is a proxy member of a proxy object or is a static proxy member.
|
||||
*
|
||||
* @param member the member to check
|
||||
* @param object the object to check
|
||||
* @return true if the member is a proxy member
|
||||
*/
|
||||
boolean isProxyMember(Member member, Object object);
|
||||
|
||||
/**
|
||||
* Check whether the given object is a Hibernate proxy.
|
||||
*
|
||||
* @param object the object to check
|
||||
* @return true if the object is a Hibernate proxy
|
||||
*/
|
||||
boolean isHibernateProxy(Object object);
|
||||
|
||||
/**
|
||||
* Check whether the given member is a member of a Hibernate proxy.
|
||||
*
|
||||
* @param member the member to check
|
||||
* @return true if the member is a Hibernate proxy member
|
||||
*/
|
||||
boolean isHibernateProxyMember(Member member);
|
||||
|
||||
/**
|
||||
* Get the target instance of the given object if it is a Hibernate proxy object,
|
||||
* otherwise return the given object.
|
||||
*
|
||||
* @param object the object to check
|
||||
* @return the target instance or the original object
|
||||
*/
|
||||
Object getHibernateProxyTarget(Object object);
|
||||
|
||||
/**
|
||||
* Resolve matching member on target class.
|
||||
*
|
||||
* @param proxyMember the proxy member
|
||||
* @param targetClass the target class
|
||||
* @return matching member on target object if one exists, otherwise the same member
|
||||
*/
|
||||
Member resolveTargetMember(Member proxyMember, Class<?> targetClass);
|
||||
|
||||
/**
|
||||
* @param proxyMember the proxy member
|
||||
* @param target the target object
|
||||
* @return matching member on target object if one exists, otherwise the same member
|
||||
* @deprecated since 7.1, use {@link #resolveTargetMember(Member, Class)} instead.
|
||||
*/
|
||||
@Deprecated
|
||||
Member resolveTargetMember(Member proxyMember, Object target);
|
||||
}
|
||||
@@ -43,47 +43,63 @@ import static java.lang.reflect.Modifier.isStatic;
|
||||
/**
|
||||
* <code>ProxyUtil</code>
|
||||
* <p>
|
||||
* Various utility methods dealing with proxies
|
||||
* Various utility methods dealing with proxies.
|
||||
* </p>
|
||||
*
|
||||
* @deprecated since 7.2, inject {@link ProxyService} instead. This class will be removed in a future version.
|
||||
*/
|
||||
@Deprecated(since = "7.2")
|
||||
public class ProxyUtil {
|
||||
private static final int CACHE_MAX_SIZE = 10000;
|
||||
private static final int CACHE_INITIAL_CAPACITY = 256;
|
||||
private static final OgnlCache<Class<?>, Boolean> isProxyCache = new DefaultOgnlCacheFactory<Class<?>, Boolean>(
|
||||
CACHE_MAX_SIZE, OgnlCacheFactory.CacheType.WTLFU, CACHE_INITIAL_CAPACITY).buildOgnlCache();
|
||||
CACHE_MAX_SIZE, OgnlCacheFactory.CacheType.BASIC, CACHE_INITIAL_CAPACITY).buildOgnlCache();
|
||||
private static final OgnlCache<Member, Boolean> isProxyMemberCache = new DefaultOgnlCacheFactory<Member, Boolean>(
|
||||
CACHE_MAX_SIZE, OgnlCacheFactory.CacheType.WTLFU, CACHE_INITIAL_CAPACITY).buildOgnlCache();
|
||||
private static final OgnlCache<Object, Class<?>> targetClassCache = new DefaultOgnlCacheFactory<Object, Class<?>>(
|
||||
CACHE_MAX_SIZE, OgnlCacheFactory.CacheType.WTLFU, CACHE_INITIAL_CAPACITY).buildOgnlCache();
|
||||
CACHE_MAX_SIZE, OgnlCacheFactory.CacheType.BASIC, CACHE_INITIAL_CAPACITY).buildOgnlCache();
|
||||
|
||||
private static final boolean HIBERNATE_AVAILABLE = isHibernateAvailable();
|
||||
|
||||
private static boolean isHibernateAvailable() {
|
||||
try {
|
||||
Class.forName("org.hibernate.proxy.HibernateProxy");
|
||||
return true;
|
||||
} catch (ClassNotFoundException e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine the ultimate target class of the given instance, traversing
|
||||
* not only a top-level proxy but any number of nested proxies as well —
|
||||
* as long as possible without side effects.
|
||||
*
|
||||
* @param candidate the instance to check (might be a proxy)
|
||||
* @return the ultimate target class (or the plain class of the given
|
||||
* object as fallback; never {@code null})
|
||||
* @deprecated since 7.2, inject {@link ProxyService} instead
|
||||
*/
|
||||
@Deprecated(since = "7.2")
|
||||
public static Class<?> ultimateTargetClass(Object candidate) {
|
||||
return targetClassCache.computeIfAbsent(candidate, k -> {
|
||||
Class<?> result = null;
|
||||
if (isSpringAopProxy(k)) {
|
||||
result = springUltimateTargetClass(k);
|
||||
} else if (isHibernateProxy(k)) {
|
||||
result = getHibernateProxyTarget(k).getClass();
|
||||
}
|
||||
if (result == null) {
|
||||
result = k.getClass();
|
||||
}
|
||||
return result;
|
||||
});
|
||||
Class<?> result = null;
|
||||
if (isSpringAopProxy(candidate)) {
|
||||
result = springUltimateTargetClass(candidate);
|
||||
} else if (isHibernateProxy(candidate)) {
|
||||
result = getHibernateProxyTarget(candidate).getClass();
|
||||
}
|
||||
if (result == null) {
|
||||
result = candidate.getClass();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether the given object is a proxy.
|
||||
*
|
||||
* @param object the object to check
|
||||
* @return true if the object is a Spring AOP or Hibernate proxy
|
||||
* @deprecated since 7.2, inject {@link ProxyService} instead
|
||||
*/
|
||||
@Deprecated(since = "7.2")
|
||||
public static boolean isProxy(Object object) {
|
||||
if (object == null) return false;
|
||||
return isProxyCache.computeIfAbsent(object.getClass(),
|
||||
@@ -92,9 +108,13 @@ public class ProxyUtil {
|
||||
|
||||
/**
|
||||
* Check whether the given member is a proxy member of a proxy object or is a static proxy member.
|
||||
*
|
||||
* @param member the member to check
|
||||
* @param object the object to check
|
||||
* @return true if the member is a proxy member
|
||||
* @deprecated since 7.2, inject {@link ProxyService} instead
|
||||
*/
|
||||
@Deprecated(since = "7.2")
|
||||
public static boolean isProxyMember(Member member, Object object) {
|
||||
if (!isStatic(member.getModifiers()) && !isProxy(object)) {
|
||||
return false;
|
||||
@@ -107,10 +127,14 @@ public class ProxyUtil {
|
||||
* Check whether the given object is a Hibernate proxy.
|
||||
*
|
||||
* @param object the object to check
|
||||
* @return true if the object is a Hibernate proxy
|
||||
* @deprecated since 7.2, inject {@link ProxyService} instead
|
||||
*/
|
||||
@Deprecated(since = "7.2")
|
||||
public static boolean isHibernateProxy(Object object) {
|
||||
if (!HIBERNATE_AVAILABLE || object == null) return false;
|
||||
try {
|
||||
return object != null && HibernateProxy.class.isAssignableFrom(object.getClass());
|
||||
return HibernateProxy.class.isAssignableFrom(object.getClass());
|
||||
} catch (LinkageError ignored) {
|
||||
return false;
|
||||
}
|
||||
@@ -120,8 +144,12 @@ public class ProxyUtil {
|
||||
* Check whether the given member is a member of a Hibernate proxy.
|
||||
*
|
||||
* @param member the member to check
|
||||
* @return true if the member is a Hibernate proxy member
|
||||
* @deprecated since 7.2, inject {@link ProxyService} instead
|
||||
*/
|
||||
@Deprecated(since = "7.2")
|
||||
public static boolean isHibernateProxyMember(Member member) {
|
||||
if (!HIBERNATE_AVAILABLE) return false;
|
||||
try {
|
||||
return hasMember(HibernateProxy.class, member);
|
||||
} catch (LinkageError ignored) {
|
||||
@@ -133,6 +161,7 @@ public class ProxyUtil {
|
||||
* Determine the ultimate target class of the given spring bean instance, traversing
|
||||
* not only a top-level spring proxy but any number of nested spring proxies as well —
|
||||
* as long as possible without side effects, that is, just for singleton targets.
|
||||
*
|
||||
* @param candidate the instance to check (might be a spring AOP proxy)
|
||||
* @return the ultimate target class (or the plain class of the given
|
||||
* object as fallback; never {@code null})
|
||||
@@ -147,6 +176,7 @@ public class ProxyUtil {
|
||||
|
||||
/**
|
||||
* Check whether the given object is a Spring proxy.
|
||||
*
|
||||
* @param object the object to check
|
||||
*/
|
||||
private static boolean isSpringAopProxy(Object object) {
|
||||
@@ -159,6 +189,7 @@ public class ProxyUtil {
|
||||
|
||||
/**
|
||||
* Check whether the given member is a member of a spring proxy.
|
||||
*
|
||||
* @param member the member to check
|
||||
*/
|
||||
private static boolean isSpringProxyMember(Member member) {
|
||||
@@ -176,7 +207,8 @@ public class ProxyUtil {
|
||||
|
||||
/**
|
||||
* Check whether the given class has a given member.
|
||||
* @param clazz the class to check
|
||||
*
|
||||
* @param clazz the class to check
|
||||
* @param member the member to check
|
||||
*/
|
||||
private static boolean hasMember(Class<?> clazz, Member member) {
|
||||
@@ -193,9 +225,15 @@ public class ProxyUtil {
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the target instance of the given object if it is a Hibernate proxy object.
|
||||
*
|
||||
* @param object the object to check
|
||||
* @return the target instance of the given object if it is a Hibernate proxy object, otherwise the given object
|
||||
* @deprecated since 7.2, inject {@link ProxyService} instead
|
||||
*/
|
||||
@Deprecated(since = "7.2")
|
||||
public static Object getHibernateProxyTarget(Object object) {
|
||||
if (!HIBERNATE_AVAILABLE) return object;
|
||||
try {
|
||||
return Hibernate.unproxy(object);
|
||||
} catch (LinkageError ignored) {
|
||||
@@ -204,9 +242,15 @@ public class ProxyUtil {
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve matching member on target object.
|
||||
*
|
||||
* @param proxyMember the proxy member
|
||||
* @param target the target object
|
||||
* @return matching member on target object if one exists, otherwise the same member
|
||||
* @deprecated since 7.1, use {@link #resolveTargetMember(Member, Class)} instead.
|
||||
* Since 7.2, inject {@link ProxyService} instead.
|
||||
*/
|
||||
@Deprecated
|
||||
@Deprecated(since = "7.1")
|
||||
public static Member resolveTargetMember(Member proxyMember, Object target) {
|
||||
return resolveTargetMember(proxyMember, target.getClass());
|
||||
}
|
||||
|
||||
@@ -0,0 +1,214 @@
|
||||
/*
|
||||
* 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.util;
|
||||
|
||||
import org.apache.commons.lang3.reflect.ConstructorUtils;
|
||||
import org.apache.commons.lang3.reflect.FieldUtils;
|
||||
import org.apache.commons.lang3.reflect.MethodUtils;
|
||||
import org.apache.struts2.inject.Inject;
|
||||
import org.apache.struts2.ognl.OgnlCache;
|
||||
import org.apache.struts2.ognl.ProxyCacheFactory;
|
||||
import org.hibernate.Hibernate;
|
||||
import org.hibernate.proxy.HibernateProxy;
|
||||
import org.springframework.aop.TargetClassAware;
|
||||
import org.springframework.aop.framework.Advised;
|
||||
import org.springframework.aop.framework.AopProxyUtils;
|
||||
import org.springframework.aop.support.AopUtils;
|
||||
import org.springframework.aop.SpringProxy;
|
||||
|
||||
import java.lang.reflect.Constructor;
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.Member;
|
||||
import java.lang.reflect.Method;
|
||||
|
||||
import static java.lang.reflect.Modifier.isPublic;
|
||||
import static java.lang.reflect.Modifier.isStatic;
|
||||
|
||||
/**
|
||||
* Default implementation of {@link ProxyService}.
|
||||
* Provides proxy detection and resolution for Spring AOP and Hibernate proxies.
|
||||
*
|
||||
* @since 7.2.0
|
||||
*/
|
||||
public class StrutsProxyService implements ProxyService {
|
||||
|
||||
private static final boolean HIBERNATE_AVAILABLE = isHibernateAvailable();
|
||||
|
||||
private static boolean isHibernateAvailable() {
|
||||
try {
|
||||
Class.forName("org.hibernate.proxy.HibernateProxy");
|
||||
return true;
|
||||
} catch (ClassNotFoundException e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private final OgnlCache<Class<?>, Boolean> isProxyCache;
|
||||
private final OgnlCache<Member, Boolean> isProxyMemberCache;
|
||||
|
||||
@Inject
|
||||
@SuppressWarnings("unchecked")
|
||||
public StrutsProxyService(ProxyCacheFactory<?, ?> proxyCacheFactory) {
|
||||
this.isProxyCache = (OgnlCache<Class<?>, Boolean>) proxyCacheFactory.buildOgnlCache();
|
||||
this.isProxyMemberCache = (OgnlCache<Member, Boolean>) proxyCacheFactory.buildOgnlCache();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<?> ultimateTargetClass(Object candidate) {
|
||||
Class<?> result = null;
|
||||
if (isSpringAopProxy(candidate)) {
|
||||
result = springUltimateTargetClass(candidate);
|
||||
} else if (isHibernateProxy(candidate)) {
|
||||
result = getHibernateProxyTarget(candidate).getClass();
|
||||
}
|
||||
if (result == null) {
|
||||
result = candidate.getClass();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isProxy(Object object) {
|
||||
if (object == null) return false;
|
||||
return isProxyCache.computeIfAbsent(object.getClass(),
|
||||
k -> isSpringAopProxy(object) || isHibernateProxy(object));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isProxyMember(Member member, Object object) {
|
||||
if (!isStatic(member.getModifiers()) && !isProxy(object)) {
|
||||
return false;
|
||||
}
|
||||
return isProxyMemberCache.computeIfAbsent(member,
|
||||
k -> isSpringProxyMember(member) || isHibernateProxyMember(member));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isHibernateProxy(Object object) {
|
||||
if (!HIBERNATE_AVAILABLE || object == null) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
return HibernateProxy.class.isAssignableFrom(object.getClass());
|
||||
} catch (LinkageError ignored) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isHibernateProxyMember(Member member) {
|
||||
if (!HIBERNATE_AVAILABLE) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
return hasMember(HibernateProxy.class, member);
|
||||
} catch (LinkageError ignored) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getHibernateProxyTarget(Object object) {
|
||||
if (!HIBERNATE_AVAILABLE) {
|
||||
return object;
|
||||
}
|
||||
try {
|
||||
return Hibernate.unproxy(object);
|
||||
} catch (LinkageError ignored) {
|
||||
return object;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Member resolveTargetMember(Member proxyMember, Class<?> targetClass) {
|
||||
int mod = proxyMember.getModifiers();
|
||||
if (proxyMember instanceof Method) {
|
||||
if (isPublic(mod)) {
|
||||
return MethodUtils.getMatchingAccessibleMethod(targetClass, proxyMember.getName(), ((Method) proxyMember).getParameterTypes());
|
||||
} else {
|
||||
return MethodUtils.getMatchingMethod(targetClass, proxyMember.getName(), ((Method) proxyMember).getParameterTypes());
|
||||
}
|
||||
} else if (proxyMember instanceof Field) {
|
||||
return FieldUtils.getField(targetClass, proxyMember.getName(), isPublic(mod));
|
||||
} else if (proxyMember instanceof Constructor && isPublic(mod)) {
|
||||
return ConstructorUtils.getMatchingAccessibleConstructor(targetClass, ((Constructor<?>) proxyMember).getParameterTypes());
|
||||
}
|
||||
return proxyMember;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Deprecated
|
||||
public Member resolveTargetMember(Member proxyMember, Object target) {
|
||||
return resolveTargetMember(proxyMember, target.getClass());
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine the ultimate target class of the given spring bean instance.
|
||||
*/
|
||||
private Class<?> springUltimateTargetClass(Object candidate) {
|
||||
try {
|
||||
return AopProxyUtils.ultimateTargetClass(candidate);
|
||||
} catch (LinkageError ignored) {
|
||||
return candidate.getClass();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether the given object is a Spring proxy.
|
||||
*/
|
||||
private boolean isSpringAopProxy(Object object) {
|
||||
try {
|
||||
return AopUtils.isAopProxy(object);
|
||||
} catch (LinkageError ignored) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether the given member is a member of a spring proxy.
|
||||
*/
|
||||
private boolean isSpringProxyMember(Member member) {
|
||||
try {
|
||||
if (hasMember(Advised.class, member))
|
||||
return true;
|
||||
if (hasMember(TargetClassAware.class, member))
|
||||
return true;
|
||||
if (hasMember(SpringProxy.class, member))
|
||||
return true;
|
||||
} catch (LinkageError ignored) {
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether the given class has a given member.
|
||||
*/
|
||||
private boolean hasMember(Class<?> clazz, Member member) {
|
||||
if (member instanceof Method method) {
|
||||
return null != MethodUtils.getMatchingMethod(clazz, member.getName(), method.getParameterTypes());
|
||||
}
|
||||
if (member instanceof Field) {
|
||||
return null != FieldUtils.getField(clazz, member.getName(), true);
|
||||
}
|
||||
if (member instanceof Constructor<?> constructor) {
|
||||
return null != ConstructorUtils.getMatchingAccessibleConstructor(clazz, constructor.getParameterTypes());
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -19,6 +19,7 @@
|
||||
package org.apache.struts2.util.fs;
|
||||
|
||||
import org.apache.struts2.FileManager;
|
||||
import org.apache.struts2.dispatcher.InternalDestroyable;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
|
||||
@@ -40,7 +41,7 @@ import static java.util.Objects.requireNonNullElseGet;
|
||||
/**
|
||||
* Default implementation of {@link FileManager}
|
||||
*/
|
||||
public class DefaultFileManager implements FileManager {
|
||||
public class DefaultFileManager implements FileManager, InternalDestroyable {
|
||||
|
||||
private static final Logger LOG = LogManager.getLogger(DefaultFileManager.class);
|
||||
|
||||
@@ -56,6 +57,19 @@ public class DefaultFileManager implements FileManager {
|
||||
public DefaultFileManager() {
|
||||
}
|
||||
|
||||
public static void clearCache() {
|
||||
files.clear();
|
||||
lazyMonitoredFilesCache.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* @since 7.2.0
|
||||
*/
|
||||
@Override
|
||||
public void destroy() {
|
||||
clearCache();
|
||||
}
|
||||
|
||||
public void setReloadingConfigs(boolean reloadingConfigs) {
|
||||
if (reloadingConfigs && !this.reloadingConfigs) {
|
||||
//starting monitoring cached not-monitored files (lazy monitoring on demand because of performance)
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
package org.apache.struts2.views.jsp;
|
||||
|
||||
import org.apache.struts2.ActionContext;
|
||||
import org.apache.struts2.ActionInvocation;
|
||||
import org.apache.struts2.config.ConfigurationException;
|
||||
import org.apache.struts2.util.ValueStack;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
@@ -44,7 +45,7 @@ public class TagUtils {
|
||||
if (stack == null) {
|
||||
LOG.warn("No ValueStack in ActionContext!");
|
||||
throw new ConfigurationException("Rendering tag out of Action scope, accessing directly JSPs is not recommended! " +
|
||||
"Please read https://struts.apache.org/security/#never-expose-jsp-files-directly");
|
||||
"Please read https://struts.apache.org/security/#never-expose-jsp-files-directly");
|
||||
} else {
|
||||
LOG.trace("Adds the current PageContext to ActionContext");
|
||||
stack.getActionContext()
|
||||
@@ -52,6 +53,13 @@ public class TagUtils {
|
||||
.with(ATTRIBUTES, new AttributeMap(stack.getContext()));
|
||||
}
|
||||
|
||||
// Check for direct JSP access (stack exists but no action invocation)
|
||||
ActionInvocation ai = ActionContext.getContext().getActionInvocation();
|
||||
if (ai == null || ai.getAction() == null) {
|
||||
LOG.warn("Rendering tag out of Action scope, accessing directly JSPs is not recommended! " +
|
||||
"Please read https://struts.apache.org/security/#never-expose-jsp-files-directly");
|
||||
}
|
||||
|
||||
return stack;
|
||||
}
|
||||
|
||||
|
||||
@@ -283,6 +283,18 @@ struts.ognl.beanInfoCacheType=wtlfu
|
||||
### application-specific needs.
|
||||
struts.ognl.beanInfoCacheMaxSize=10000
|
||||
|
||||
### Specifies the type of cache to use for proxy detection. See StrutsConstants class for further information.
|
||||
struts.proxy.cacheType=wtlfu
|
||||
|
||||
### Specifies the maximum cache size for proxy detection caches.
|
||||
struts.proxy.cacheMaxSize=10000
|
||||
|
||||
### Specifies the ProxyCacheFactory implementation class.
|
||||
struts.proxy.cacheFactory=struts
|
||||
|
||||
### Specifies the ProxyService implementation class.
|
||||
struts.proxyService=struts
|
||||
|
||||
### Indicates if Dispatcher should handle unexpected exceptions by calling sendError()
|
||||
### or simply rethrow it as a ServletException to allow future processing by other frameworks like Spring Security
|
||||
struts.handle.exception=true
|
||||
@@ -319,4 +331,8 @@ struts.url.decoder=strutsUrlDecoder
|
||||
### Defines source to read nonce value from, possible values are: request, session
|
||||
struts.csp.nonceSource=session
|
||||
|
||||
### Checkbox hidden field prefix
|
||||
### Default prefix for backward compatibility. Change to "struts_checkbox_" for HTML5 validation.
|
||||
struts.ui.checkbox.hiddenPrefix=__checkbox_
|
||||
|
||||
### END SNIPPET: complete_file
|
||||
|
||||
@@ -240,6 +240,10 @@
|
||||
class="org.apache.struts2.ognl.DefaultOgnlExpressionCacheFactory" scope="singleton"/>
|
||||
<bean type="org.apache.struts2.ognl.BeanInfoCacheFactory" name="struts"
|
||||
class="org.apache.struts2.ognl.DefaultOgnlBeanInfoCacheFactory" scope="singleton"/>
|
||||
<bean type="org.apache.struts2.ognl.ProxyCacheFactory" name="struts"
|
||||
class="org.apache.struts2.ognl.StrutsProxyCacheFactory" scope="singleton"/>
|
||||
<bean type="org.apache.struts2.util.ProxyService" name="struts"
|
||||
class="org.apache.struts2.util.StrutsProxyService" scope="singleton"/>
|
||||
|
||||
<bean type="org.apache.struts2.url.QueryStringBuilder" name="strutsQueryStringBuilder"
|
||||
class="org.apache.struts2.url.StrutsQueryStringBuilder" scope="singleton"/>
|
||||
@@ -256,4 +260,22 @@
|
||||
<bean type="org.apache.struts2.interceptor.csp.CspNonceReader" name="struts"
|
||||
class="org.apache.struts2.interceptor.csp.StrutsCspNonceReader"/>
|
||||
|
||||
<!-- WW-5537: InternalDestroyable beans for automatic cleanup during undeploy -->
|
||||
<bean type="org.apache.struts2.dispatcher.InternalDestroyable" name="componentCache"
|
||||
class="org.apache.struts2.dispatcher.ComponentCacheDestroyable"/>
|
||||
<bean type="org.apache.struts2.dispatcher.InternalDestroyable" name="compoundRootAccessor"
|
||||
class="org.apache.struts2.ognl.accessor.CompoundRootAccessor"/>
|
||||
<bean type="org.apache.struts2.dispatcher.InternalDestroyable" name="defaultFileManager"
|
||||
class="org.apache.struts2.util.fs.DefaultFileManager"/>
|
||||
<bean type="org.apache.struts2.dispatcher.InternalDestroyable" name="scopeInterceptorCache"
|
||||
class="org.apache.struts2.dispatcher.ScopeInterceptorCacheDestroyable"/>
|
||||
<bean type="org.apache.struts2.dispatcher.InternalDestroyable" name="ognlCache"
|
||||
class="org.apache.struts2.dispatcher.OgnlCacheDestroyable"/>
|
||||
<bean type="org.apache.struts2.dispatcher.InternalDestroyable" name="finalizableReferenceQueue"
|
||||
class="org.apache.struts2.dispatcher.FinalizableReferenceQueueDestroyable"/>
|
||||
<bean type="org.apache.struts2.dispatcher.InternalDestroyable" name="freemarkerCache"
|
||||
class="org.apache.struts2.dispatcher.FreemarkerCacheDestroyable"/>
|
||||
<bean type="org.apache.struts2.dispatcher.InternalDestroyable" name="debugUtilsCache"
|
||||
class="org.apache.struts2.dispatcher.DebugUtilsCacheDestroyable"/>
|
||||
|
||||
</struts>
|
||||
|
||||
@@ -41,7 +41,7 @@
|
||||
<#include "/${attributes.templateDir}/${attributes.expandTheme}/dynamic-attributes.ftl" /><#rt/>
|
||||
/><#rt/>
|
||||
<#if attributes.submitUnchecked!false>
|
||||
<input type="hidden" id="__checkbox_${attributes.id}" name="__checkbox_${attributes.name}" value="${attributes.fieldValue}"<#rt/>
|
||||
<input type="hidden" id="${attributes.hiddenPrefix}${attributes.id}" name="${attributes.hiddenPrefix}${attributes.name}" value="${attributes.fieldValue}"<#rt/>
|
||||
<#if attributes.disabled!false>
|
||||
disabled="disabled"<#rt/>
|
||||
</#if>
|
||||
|
||||
@@ -40,7 +40,7 @@
|
||||
<#include "/${attributes.templateDir}/${attributes.expandTheme}/dynamic-attributes.ftl" />
|
||||
/><#rt/>
|
||||
<#if attributes.submitUnchecked!false>
|
||||
<input type="hidden" id="__checkbox_${attributes.id}" name="__checkbox_${attributes.name}" value="${attributes.fieldValue}"<#rt/>
|
||||
<input type="hidden" id="${attributes.hiddenPrefix}${attributes.id}" name="${attributes.hiddenPrefix}${attributes.name}" value="${attributes.fieldValue}"<#rt/>
|
||||
<#if attributes.disabled!false>
|
||||
disabled="disabled"<#rt/>
|
||||
</#if>
|
||||
|
||||
@@ -18,25 +18,67 @@
|
||||
*/
|
||||
package org.apache.struts2;
|
||||
|
||||
import org.apache.struts2.mock.MockActionInvocation;
|
||||
import org.apache.struts2.StrutsInternalTestCase;
|
||||
import org.apache.struts2.config.ConfigurationException;
|
||||
import org.apache.struts2.config.StrutsXmlConfigurationProvider;
|
||||
import org.junit.Test;
|
||||
import org.apache.struts2.mock.MockActionInvocation;
|
||||
|
||||
public class DefaultActionProxyTest extends StrutsInternalTestCase {
|
||||
|
||||
@Test
|
||||
public void testThorwExceptionOnNotAllowedMethod() throws Exception {
|
||||
final String filename = "org/apache/struts2/config/providers/xwork-test-allowed-methods.xml";
|
||||
loadConfigurationProviders(new StrutsXmlConfigurationProvider(filename));
|
||||
private static final String CONFIG = "org/apache/struts2/config/providers/xwork-test-allowed-methods.xml";
|
||||
|
||||
public void testThrowExceptionOnNotAllowedMethod() {
|
||||
loadConfigurationProviders(new StrutsXmlConfigurationProvider(CONFIG));
|
||||
DefaultActionProxy dap = new DefaultActionProxy(new MockActionInvocation(), "strict", "Default", "notAllowed", true, true);
|
||||
container.inject(dap);
|
||||
|
||||
try {
|
||||
dap.prepare();
|
||||
fail("Must throw exception!");
|
||||
} catch (Exception e) {
|
||||
assertEquals(e.getMessage(), "Method notAllowed for action Default is not allowed!");
|
||||
} catch (ConfigurationException e) {
|
||||
assertEquals("Method notAllowed for action Default is not allowed!", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public void testMethodSpecifiedWhenPassedExplicitly() {
|
||||
loadConfigurationProviders(new StrutsXmlConfigurationProvider(CONFIG));
|
||||
DefaultActionProxy dap = new DefaultActionProxy(new MockActionInvocation(), "default", "Default", "input", true, true);
|
||||
container.inject(dap);
|
||||
dap.prepare();
|
||||
|
||||
assertTrue("Method should be specified when passed as constructor argument", dap.isMethodSpecified());
|
||||
assertEquals("input", dap.getMethod());
|
||||
}
|
||||
|
||||
public void testMethodSpecifiedWhenResolvedFromConfig() {
|
||||
loadConfigurationProviders(new StrutsXmlConfigurationProvider(CONFIG));
|
||||
// ConfigMethod action has method="onPostOnly" in XML config, no method passed in constructor
|
||||
DefaultActionProxy dap = new DefaultActionProxy(new MockActionInvocation(), "default", "ConfigMethod", null, true, true);
|
||||
container.inject(dap);
|
||||
dap.prepare();
|
||||
|
||||
assertTrue("Method should be specified when resolved from action config", dap.isMethodSpecified());
|
||||
assertEquals("onPostOnly", dap.getMethod());
|
||||
}
|
||||
|
||||
public void testMethodNotSpecifiedWhenDefaultingToExecute() {
|
||||
loadConfigurationProviders(new StrutsXmlConfigurationProvider(CONFIG));
|
||||
// NoMethod action has no method in XML config and no method passed in constructor
|
||||
DefaultActionProxy dap = new DefaultActionProxy(new MockActionInvocation(), "default", "NoMethod", null, true, true);
|
||||
container.inject(dap);
|
||||
dap.prepare();
|
||||
|
||||
assertFalse("Method should not be specified when defaulting to execute", dap.isMethodSpecified());
|
||||
assertEquals("execute", dap.getMethod());
|
||||
}
|
||||
|
||||
public void testMethodSpecifiedWithWildcardAction() {
|
||||
loadConfigurationProviders(new StrutsXmlConfigurationProvider(CONFIG));
|
||||
// Wild-onPostOnly matches Wild-* with method="{1}" -> resolves to "onPostOnly"
|
||||
DefaultActionProxy dap = new DefaultActionProxy(new MockActionInvocation(), "default", "Wild-onPostOnly", null, true, true);
|
||||
container.inject(dap);
|
||||
dap.prepare();
|
||||
|
||||
assertTrue("Method should be specified when resolved from wildcard config", dap.isMethodSpecified());
|
||||
assertEquals("onPostOnly", dap.getMethod());
|
||||
}
|
||||
}
|
||||
@@ -337,6 +337,38 @@ public class ConfigurationTest extends XWorkTestCase {
|
||||
}
|
||||
|
||||
|
||||
public void testDefaultActionRefWithWildcard() {
|
||||
RuntimeConfiguration configuration = configurationManager.getConfiguration().getRuntimeConfiguration();
|
||||
|
||||
// "unknown-action" doesn't exist in /wildcard-default, so default-action-ref "movie-input" should be used
|
||||
// "movie-input" matches wildcard "movie-*", so it should resolve via wildcard matching
|
||||
ActionConfig config = configuration.getActionConfig("/wildcard-default", "unknown-action");
|
||||
|
||||
assertNotNull("Default action ref should resolve via wildcard matching", config);
|
||||
assertEquals("org.apache.struts2.SimpleAction", config.getClassName());
|
||||
assertEquals("input", config.getMethodName());
|
||||
}
|
||||
|
||||
public void testDefaultActionRefWithExactMatch() {
|
||||
RuntimeConfiguration configuration = configurationManager.getConfiguration().getRuntimeConfiguration();
|
||||
|
||||
// default-action-ref "home" matches an exact action, so it should resolve without wildcard matching
|
||||
ActionConfig config = configuration.getActionConfig("/exact-default", "unknown-action");
|
||||
|
||||
assertNotNull("Default action ref should resolve via exact match", config);
|
||||
assertEquals("org.apache.struts2.SimpleAction", config.getClassName());
|
||||
assertEquals("execute", config.getMethodName());
|
||||
}
|
||||
|
||||
public void testDefaultActionRefWithWildcardNoMatch() {
|
||||
RuntimeConfiguration configuration = configurationManager.getConfiguration().getRuntimeConfiguration();
|
||||
|
||||
// default-action-ref "no-match-anywhere" matches neither an exact action nor wildcard "movie-*"
|
||||
ActionConfig config = configuration.getActionConfig("/wildcard-default-nomatch", "unknown-action");
|
||||
|
||||
assertNull("Should return null when default-action-ref matches neither exact nor wildcard", config);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void setUp() throws Exception {
|
||||
super.setUp();
|
||||
|
||||
+1
-1
@@ -42,7 +42,7 @@ public class XmlConfigurationProviderAllowedMethodsTest extends ConfigurationTes
|
||||
Map actionConfigs = pkg.getActionConfigs();
|
||||
|
||||
// assertions
|
||||
assertEquals(5, actionConfigs.size());
|
||||
assertEquals(8, actionConfigs.size());
|
||||
|
||||
ActionConfig action = (ActionConfig) actionConfigs.get("Default");
|
||||
assertEquals(1, action.getAllowedMethods().size());
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
/*
|
||||
* 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.Container;
|
||||
import org.junit.After;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
||||
public class ContainerHolderTest {
|
||||
|
||||
@After
|
||||
public void tearDown() {
|
||||
ContainerHolder.clear();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void storeAndGet() {
|
||||
Container c = mock(Container.class);
|
||||
ContainerHolder.store(c);
|
||||
assertThat(ContainerHolder.get()).isSameAs(c);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void clearRemovesCurrentThread() {
|
||||
ContainerHolder.store(mock(Container.class));
|
||||
ContainerHolder.clear();
|
||||
assertThat(ContainerHolder.get()).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void invalidateAllMakesOtherThreadsSeeNull() throws Exception {
|
||||
Container c = mock(Container.class);
|
||||
|
||||
// Another thread stores a container
|
||||
Thread t = new Thread(() -> ContainerHolder.store(c));
|
||||
t.start();
|
||||
t.join();
|
||||
|
||||
// Invalidate on main thread
|
||||
ContainerHolder.invalidateAll();
|
||||
|
||||
// Other thread's cached value should now be stale
|
||||
AtomicReference<Container> otherThreadResult = new AtomicReference<>();
|
||||
Thread t2 = new Thread(() -> otherThreadResult.set(ContainerHolder.get()));
|
||||
t2.start();
|
||||
t2.join();
|
||||
|
||||
assertThat(otherThreadResult.get()).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void invalidateAllClearsCallingThread() {
|
||||
ContainerHolder.store(mock(Container.class));
|
||||
ContainerHolder.invalidateAll();
|
||||
assertThat(ContainerHolder.get()).isNull();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,185 @@
|
||||
/*
|
||||
* 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.ActionContext;
|
||||
import org.apache.struts2.StrutsJUnit4InternalTestCase;
|
||||
import org.apache.struts2.components.Component;
|
||||
import org.apache.struts2.inject.Container;
|
||||
import org.apache.struts2.ognl.accessor.CompoundRootAccessor;
|
||||
import org.apache.struts2.util.DebugUtils;
|
||||
import org.apache.struts2.util.fs.DefaultFileManager;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.net.URI;
|
||||
import java.net.URL;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentMap;
|
||||
|
||||
import static java.util.Collections.emptyMap;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* WW-5537: Verifies that Dispatcher.cleanup() properly clears all static state
|
||||
* that could prevent classloader garbage collection during hot redeployment.
|
||||
*/
|
||||
public class DispatcherCleanupTest extends StrutsJUnit4InternalTestCase {
|
||||
|
||||
@Test
|
||||
public void cleanupDiscoversAllInternalDestroyableBeans() {
|
||||
initDispatcher(emptyMap());
|
||||
|
||||
Container container = dispatcher.getConfigurationManager().getConfiguration().getContainer();
|
||||
Set<String> names = container.getInstanceNames(InternalDestroyable.class);
|
||||
|
||||
Set<String> expected = new HashSet<>(Arrays.asList(
|
||||
"componentCache", "compoundRootAccessor", "defaultFileManager",
|
||||
"scopeInterceptorCache", "ognlCache", "finalizableReferenceQueue",
|
||||
"freemarkerCache", "debugUtilsCache"
|
||||
));
|
||||
assertThat(names).containsAll(expected);
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
public void cleanupClearsComponentStandardAttributesMap() throws Exception {
|
||||
initDispatcher(emptyMap());
|
||||
|
||||
Field mapField = Component.class.getDeclaredField("standardAttributesMap");
|
||||
mapField.setAccessible(true);
|
||||
ConcurrentMap<Class<?>, Collection<String>> map =
|
||||
(ConcurrentMap<Class<?>, Collection<String>>) mapField.get(null);
|
||||
|
||||
map.put(String.class, new ArrayList<>());
|
||||
assertThat(map).isNotEmpty();
|
||||
|
||||
dispatcher.cleanup();
|
||||
|
||||
assertThat(map).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
public void cleanupClearsCompoundRootAccessorCache() throws Exception {
|
||||
initDispatcher(emptyMap());
|
||||
|
||||
Field field = CompoundRootAccessor.class.getDeclaredField("invalidMethods");
|
||||
field.setAccessible(true);
|
||||
Map<Object, Boolean> invalidMethods = (Map<Object, Boolean>) field.get(null);
|
||||
|
||||
invalidMethods.put("testKey", Boolean.TRUE);
|
||||
assertThat(invalidMethods).isNotEmpty();
|
||||
|
||||
dispatcher.cleanup();
|
||||
|
||||
assertThat(invalidMethods).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void cleanupClearsDefaultFileManagerFilesMap() throws Exception {
|
||||
initDispatcher(emptyMap());
|
||||
|
||||
Field filesField = DefaultFileManager.class.getDeclaredField("files");
|
||||
filesField.setAccessible(true);
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, Object> files = (Map<String, Object>) filesField.get(null);
|
||||
|
||||
files.put("test-key", new Object());
|
||||
assertThat(files).isNotEmpty();
|
||||
|
||||
dispatcher.cleanup();
|
||||
|
||||
assertThat(files).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void cleanupClearsDefaultFileManagerLazyCache() throws Exception {
|
||||
initDispatcher(emptyMap());
|
||||
|
||||
Field lazyCacheField = DefaultFileManager.class.getDeclaredField("lazyMonitoredFilesCache");
|
||||
lazyCacheField.setAccessible(true);
|
||||
@SuppressWarnings("unchecked")
|
||||
List<URL> lazyCache = (List<URL>) lazyCacheField.get(null);
|
||||
|
||||
lazyCache.add(new URI("file:///test").toURL());
|
||||
assertThat(lazyCache).isNotEmpty();
|
||||
|
||||
dispatcher.cleanup();
|
||||
|
||||
assertThat(lazyCache).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void cleanupClearsDispatcherListeners() throws Exception {
|
||||
initDispatcher(emptyMap());
|
||||
|
||||
Dispatcher.addDispatcherListener(new DispatcherListener() {
|
||||
@Override
|
||||
public void dispatcherInitialized(Dispatcher du) {
|
||||
// intentionally empty — test only verifies listener list is cleared
|
||||
}
|
||||
@Override
|
||||
public void dispatcherDestroyed(Dispatcher du) {
|
||||
// intentionally empty — test only verifies listener list is cleared
|
||||
}
|
||||
});
|
||||
|
||||
dispatcher.cleanup();
|
||||
|
||||
Field listenersField = Dispatcher.class.getDeclaredField("dispatcherListeners");
|
||||
listenersField.setAccessible(true);
|
||||
List<?> listeners = (List<?>) listenersField.get(null);
|
||||
assertThat(listeners).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void cleanupClearsThreadLocals() {
|
||||
assertThat(Dispatcher.getInstance()).isNotNull();
|
||||
assertThat(ActionContext.getContext()).isNotNull();
|
||||
|
||||
dispatcher.cleanup();
|
||||
|
||||
assertThat(Dispatcher.getInstance()).isNull();
|
||||
assertThat(ActionContext.getContext()).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
public void cleanupClearsDebugUtilsCache() throws Exception {
|
||||
initDispatcher(emptyMap());
|
||||
|
||||
Field field = DebugUtils.class.getDeclaredField("IS_LOGGED");
|
||||
field.setAccessible(true);
|
||||
Set<String> isLogged = (Set<String>) field.get(null);
|
||||
|
||||
isLogged.add("test-key");
|
||||
assertThat(isLogged).isNotEmpty();
|
||||
|
||||
dispatcher.cleanup();
|
||||
|
||||
assertThat(isLogged).isEmpty();
|
||||
}
|
||||
}
|
||||
@@ -38,171 +38,227 @@ public class CheckboxInterceptorTest extends StrutsInternalTestCase {
|
||||
private Map<String, Object> param;
|
||||
|
||||
protected void setUp() throws Exception {
|
||||
super.setUp();
|
||||
param = new HashMap<>();
|
||||
super.setUp();
|
||||
param = new HashMap<>();
|
||||
|
||||
interceptor = new CheckboxInterceptor();
|
||||
ai = new MockActionInvocation();
|
||||
ai.setInvocationContext(ActionContext.getContext());
|
||||
interceptor = new CheckboxInterceptor();
|
||||
ai = new MockActionInvocation();
|
||||
ai.setInvocationContext(ActionContext.getContext());
|
||||
}
|
||||
|
||||
private void prepare(ActionInvocation ai) {
|
||||
ai.getInvocationContext().withParameters(HttpParameters.create(param).build());
|
||||
}
|
||||
private void prepare(ActionInvocation ai) {
|
||||
ai.getInvocationContext().withParameters(HttpParameters.create(param).build());
|
||||
}
|
||||
|
||||
public void testNoParam() throws Exception {
|
||||
prepare(ai);
|
||||
public void testNoParam() throws Exception {
|
||||
prepare(ai);
|
||||
|
||||
interceptor.init();
|
||||
interceptor.intercept(ai);
|
||||
interceptor.destroy();
|
||||
interceptor.init();
|
||||
interceptor.intercept(ai);
|
||||
interceptor.destroy();
|
||||
|
||||
assertEquals(0, param.size());
|
||||
}
|
||||
assertEquals(0, param.size());
|
||||
}
|
||||
|
||||
public void testPassthroughOne() throws Exception {
|
||||
param.put("user", "batman");
|
||||
public void testPassthroughOne() throws Exception {
|
||||
param.put("user", "batman");
|
||||
|
||||
prepare(ai);
|
||||
prepare(ai);
|
||||
|
||||
interceptor.init();
|
||||
interceptor.intercept(ai);
|
||||
interceptor.destroy();
|
||||
interceptor.init();
|
||||
interceptor.intercept(ai);
|
||||
interceptor.destroy();
|
||||
|
||||
assertEquals(1, ai.getInvocationContext().getParameters().keySet().size());
|
||||
}
|
||||
assertEquals(1, ai.getInvocationContext().getParameters().size());
|
||||
}
|
||||
|
||||
public void testPassthroughTwo() throws Exception {
|
||||
param.put("user", "batman");
|
||||
param.put("email", "batman@comic.org");
|
||||
public void testPassthroughTwo() throws Exception {
|
||||
param.put("user", "batman");
|
||||
param.put("email", "batman@comic.org");
|
||||
|
||||
prepare(ai);
|
||||
prepare(ai);
|
||||
|
||||
interceptor.init();
|
||||
interceptor.intercept(ai);
|
||||
interceptor.destroy();
|
||||
interceptor.init();
|
||||
interceptor.intercept(ai);
|
||||
interceptor.destroy();
|
||||
|
||||
assertEquals(2, ai.getInvocationContext().getParameters().keySet().size());
|
||||
}
|
||||
assertEquals(2, ai.getInvocationContext().getParameters().size());
|
||||
}
|
||||
|
||||
public void testOneCheckboxTrue() throws Exception {
|
||||
param.put("user", "batman");
|
||||
param.put("email", "batman@comic.org");
|
||||
param.put("superpower", "true");
|
||||
param.put("__checkbox_superpower", "true");
|
||||
assertTrue(param.containsKey("__checkbox_superpower"));
|
||||
public void testOneCheckboxTrue() throws Exception {
|
||||
param.put("user", "batman");
|
||||
param.put("email", "batman@comic.org");
|
||||
param.put("superpower", "true");
|
||||
param.put("__checkbox_superpower", "true");
|
||||
assertTrue(param.containsKey("__checkbox_superpower"));
|
||||
|
||||
prepare(ai);
|
||||
prepare(ai);
|
||||
|
||||
interceptor.init();
|
||||
interceptor.intercept(ai);
|
||||
interceptor.destroy();
|
||||
interceptor.init();
|
||||
interceptor.intercept(ai);
|
||||
interceptor.destroy();
|
||||
|
||||
HttpParameters parameters = ai.getInvocationContext().getParameters();
|
||||
assertFalse(parameters.contains("__checkbox_superpower"));
|
||||
assertEquals(3, parameters.keySet().size()); // should be 3 as __checkbox_ should be removed
|
||||
assertEquals("true", parameters.get("superpower").getValue());
|
||||
}
|
||||
HttpParameters parameters = ai.getInvocationContext().getParameters();
|
||||
assertFalse(parameters.contains("__checkbox_superpower"));
|
||||
assertEquals(3, parameters.size()); // should be 3 as __checkbox_ should be removed
|
||||
assertEquals("true", parameters.get("superpower").getValue());
|
||||
}
|
||||
|
||||
public void testOneCheckboxNoValue() throws Exception {
|
||||
param.put("user", "batman");
|
||||
param.put("email", "batman@comic.org");
|
||||
param.put("__checkbox_superpower", "false");
|
||||
assertTrue(param.containsKey("__checkbox_superpower"));
|
||||
public void testOneCheckboxNoValue() throws Exception {
|
||||
param.put("user", "batman");
|
||||
param.put("email", "batman@comic.org");
|
||||
param.put("__checkbox_superpower", "false");
|
||||
assertTrue(param.containsKey("__checkbox_superpower"));
|
||||
|
||||
prepare(ai);
|
||||
prepare(ai);
|
||||
|
||||
interceptor.init();
|
||||
interceptor.intercept(ai);
|
||||
interceptor.destroy();
|
||||
interceptor.init();
|
||||
interceptor.intercept(ai);
|
||||
interceptor.destroy();
|
||||
|
||||
HttpParameters parameters = ai.getInvocationContext().getParameters();
|
||||
assertFalse(parameters.contains("__checkbox_superpower"));
|
||||
assertEquals(3, parameters.keySet().size()); // should be 3 as __checkbox_ should be removed
|
||||
assertEquals("false", parameters.get("superpower").getValue());
|
||||
}
|
||||
HttpParameters parameters = ai.getInvocationContext().getParameters();
|
||||
assertFalse(parameters.contains("__checkbox_superpower"));
|
||||
assertEquals(3, parameters.size()); // should be 3 as __checkbox_ should be removed
|
||||
assertEquals("false", parameters.get("superpower").getValue());
|
||||
}
|
||||
|
||||
public void testOneCheckboxNoValueDifferentDefault() throws Exception {
|
||||
param.put("user", "batman");
|
||||
param.put("email", "batman@comic.org");
|
||||
param.put("__checkbox_superpower", "false");
|
||||
assertTrue(param.containsKey("__checkbox_superpower"));
|
||||
public void testOneCheckboxNoValueDifferentDefault() throws Exception {
|
||||
param.put("user", "batman");
|
||||
param.put("email", "batman@comic.org");
|
||||
param.put("__checkbox_superpower", "false");
|
||||
assertTrue(param.containsKey("__checkbox_superpower"));
|
||||
|
||||
prepare(ai);
|
||||
prepare(ai);
|
||||
|
||||
interceptor.setUncheckedValue("off");
|
||||
interceptor.init();
|
||||
interceptor.intercept(ai);
|
||||
interceptor.destroy();
|
||||
interceptor.setUncheckedValue("off");
|
||||
interceptor.init();
|
||||
interceptor.intercept(ai);
|
||||
interceptor.destroy();
|
||||
|
||||
HttpParameters parameters = ai.getInvocationContext().getParameters();
|
||||
assertFalse(parameters.contains("__checkbox_superpower"));
|
||||
assertEquals(3, parameters.keySet().size()); // should be 3 as __checkbox_ should be removed
|
||||
assertEquals("off", parameters.get("superpower").getValue());
|
||||
}
|
||||
HttpParameters parameters = ai.getInvocationContext().getParameters();
|
||||
assertFalse(parameters.contains("__checkbox_superpower"));
|
||||
assertEquals(3, parameters.size()); // should be 3 as __checkbox_ should be removed
|
||||
assertEquals("off", parameters.get("superpower").getValue());
|
||||
}
|
||||
|
||||
public void testTwoCheckboxNoValue() throws Exception {
|
||||
param.put("user", "batman");
|
||||
param.put("email", "batman@comic.org");
|
||||
param.put("__checkbox_superpower", new String[]{"true", "true"});
|
||||
param.put("user", "batman");
|
||||
param.put("email", "batman@comic.org");
|
||||
param.put("__checkbox_superpower", new String[]{"true", "true"});
|
||||
|
||||
prepare(ai);
|
||||
prepare(ai);
|
||||
|
||||
interceptor.init();
|
||||
interceptor.intercept(ai);
|
||||
interceptor.destroy();
|
||||
interceptor.init();
|
||||
interceptor.intercept(ai);
|
||||
interceptor.destroy();
|
||||
|
||||
HttpParameters parameters = ai.getInvocationContext().getParameters();
|
||||
assertFalse(parameters.contains("__checkbox_superpower"));
|
||||
assertEquals(2, parameters.keySet().size()); // should be 2 as __checkbox_ should be removed
|
||||
assertFalse(parameters.get("superpower").isDefined());
|
||||
HttpParameters parameters = ai.getInvocationContext().getParameters();
|
||||
assertFalse(parameters.contains("__checkbox_superpower"));
|
||||
assertEquals(2, parameters.size()); // should be 2 as __checkbox_ should be removed
|
||||
assertFalse(parameters.get("superpower").isDefined());
|
||||
}
|
||||
|
||||
public void testTwoCheckboxMixed() throws Exception {
|
||||
param.put("user", "batman");
|
||||
param.put("email", "batman@comic.org");
|
||||
param.put("__checkbox_superpower", "true");
|
||||
param.put("superpower", "yes");
|
||||
param.put("__checkbox_cool", "no");
|
||||
assertTrue(param.containsKey("__checkbox_superpower"));
|
||||
assertTrue(param.containsKey("__checkbox_cool"));
|
||||
param.put("user", "batman");
|
||||
param.put("email", "batman@comic.org");
|
||||
param.put("__checkbox_superpower", "true");
|
||||
param.put("superpower", "yes");
|
||||
param.put("__checkbox_cool", "no");
|
||||
assertTrue(param.containsKey("__checkbox_superpower"));
|
||||
assertTrue(param.containsKey("__checkbox_cool"));
|
||||
|
||||
prepare(ai);
|
||||
prepare(ai);
|
||||
|
||||
interceptor.init();
|
||||
interceptor.intercept(ai);
|
||||
interceptor.destroy();
|
||||
interceptor.init();
|
||||
interceptor.intercept(ai);
|
||||
interceptor.destroy();
|
||||
|
||||
HttpParameters parameters = ai.getInvocationContext().getParameters();
|
||||
assertFalse(parameters.contains("__checkbox_superpower"));
|
||||
assertFalse(parameters.contains("__checkbox_cool"));
|
||||
assertEquals(4, parameters.keySet().size()); // should be 4 as __checkbox_ should be removed
|
||||
assertEquals("yes", parameters.get("superpower").getValue());
|
||||
assertEquals("false", parameters.get("cool").getValue()); // will use false as default and not 'no'
|
||||
}
|
||||
HttpParameters parameters = ai.getInvocationContext().getParameters();
|
||||
assertFalse(parameters.contains("__checkbox_superpower"));
|
||||
assertFalse(parameters.contains("__checkbox_cool"));
|
||||
assertEquals(4, parameters.size()); // should be 4 as __checkbox_ should be removed
|
||||
assertEquals("yes", parameters.get("superpower").getValue());
|
||||
assertEquals("false", parameters.get("cool").getValue()); // will use false as default and not 'no'
|
||||
}
|
||||
|
||||
public void testTwoCheckboxMixedWithDifferentDefault() throws Exception {
|
||||
param.put("user", "batman");
|
||||
param.put("email", "batman@comic.org");
|
||||
param.put("__checkbox_superpower", "true");
|
||||
param.put("superpower", "yes");
|
||||
param.put("__checkbox_cool", "no");
|
||||
assertTrue(param.containsKey("__checkbox_superpower"));
|
||||
assertTrue(param.containsKey("__checkbox_cool"));
|
||||
public void testTwoCheckboxMixedWithDifferentDefault() throws Exception {
|
||||
param.put("user", "batman");
|
||||
param.put("email", "batman@comic.org");
|
||||
param.put("__checkbox_superpower", "true");
|
||||
param.put("superpower", "yes");
|
||||
param.put("__checkbox_cool", "no");
|
||||
assertTrue(param.containsKey("__checkbox_superpower"));
|
||||
assertTrue(param.containsKey("__checkbox_cool"));
|
||||
|
||||
prepare(ai);
|
||||
prepare(ai);
|
||||
|
||||
interceptor.setUncheckedValue("no");
|
||||
interceptor.init();
|
||||
interceptor.intercept(ai);
|
||||
interceptor.destroy();
|
||||
interceptor.setUncheckedValue("no");
|
||||
interceptor.init();
|
||||
interceptor.intercept(ai);
|
||||
interceptor.destroy();
|
||||
|
||||
HttpParameters parameters = ai.getInvocationContext().getParameters();
|
||||
assertFalse(parameters.contains("__checkbox_superpower"));
|
||||
assertFalse(parameters.contains("__checkbox_cool"));
|
||||
assertEquals(4, parameters.keySet().size()); // should be 4 as __checkbox_ should be removed
|
||||
assertEquals("yes", parameters.get("superpower").getValue());
|
||||
assertEquals("no", parameters.get("cool").getValue());
|
||||
}
|
||||
HttpParameters parameters = ai.getInvocationContext().getParameters();
|
||||
assertFalse(parameters.contains("__checkbox_superpower"));
|
||||
assertFalse(parameters.contains("__checkbox_cool"));
|
||||
assertEquals(4, parameters.size()); // should be 4 as __checkbox_ should be removed
|
||||
assertEquals("yes", parameters.get("superpower").getValue());
|
||||
assertEquals("no", parameters.get("cool").getValue());
|
||||
}
|
||||
|
||||
public void testCustomHiddenPrefixChecked() throws Exception {
|
||||
param.put("user", "batman");
|
||||
param.put("struts_checkbox_superpower", "true");
|
||||
param.put("superpower", "yes");
|
||||
assertTrue(param.containsKey("struts_checkbox_superpower"));
|
||||
|
||||
prepare(ai);
|
||||
|
||||
interceptor.setHiddenPrefix("struts_checkbox_");
|
||||
interceptor.init();
|
||||
interceptor.intercept(ai);
|
||||
interceptor.destroy();
|
||||
|
||||
HttpParameters parameters = ai.getInvocationContext().getParameters();
|
||||
assertFalse(parameters.contains("struts_checkbox_superpower"));
|
||||
assertEquals(2, parameters.size());
|
||||
assertEquals("yes", parameters.get("superpower").getValue());
|
||||
}
|
||||
|
||||
public void testCustomHiddenPrefixUnchecked() throws Exception {
|
||||
param.put("user", "batman");
|
||||
param.put("struts_checkbox_superpower", "true");
|
||||
assertTrue(param.containsKey("struts_checkbox_superpower"));
|
||||
|
||||
prepare(ai);
|
||||
|
||||
interceptor.setHiddenPrefix("struts_checkbox_");
|
||||
interceptor.init();
|
||||
interceptor.intercept(ai);
|
||||
interceptor.destroy();
|
||||
|
||||
HttpParameters parameters = ai.getInvocationContext().getParameters();
|
||||
assertFalse(parameters.contains("struts_checkbox_superpower"));
|
||||
assertEquals(2, parameters.size());
|
||||
assertEquals("false", parameters.get("superpower").getValue());
|
||||
}
|
||||
|
||||
public void testCustomHiddenPrefixIgnoresDefaultPrefix() throws Exception {
|
||||
param.put("user", "batman");
|
||||
param.put("__checkbox_superpower", "true");
|
||||
assertTrue(param.containsKey("__checkbox_superpower"));
|
||||
|
||||
prepare(ai);
|
||||
|
||||
interceptor.setHiddenPrefix("struts_checkbox_");
|
||||
interceptor.init();
|
||||
interceptor.intercept(ai);
|
||||
interceptor.destroy();
|
||||
|
||||
HttpParameters parameters = ai.getInvocationContext().getParameters();
|
||||
// With custom prefix, the default __checkbox_ prefix should be ignored
|
||||
assertTrue(parameters.contains("__checkbox_superpower"));
|
||||
assertEquals(2, parameters.size());
|
||||
assertFalse(parameters.get("superpower").isDefined());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -35,6 +35,7 @@ import org.springframework.mock.web.MockHttpSession;
|
||||
|
||||
import jakarta.servlet.http.Cookie;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
@@ -100,7 +101,7 @@ public class I18nInterceptorTest extends TestCase {
|
||||
|
||||
assertFalse(mai.getInvocationContext().getParameters().get(I18nInterceptor.DEFAULT_PARAMETER).isDefined()); // should have been removed
|
||||
|
||||
Locale denmark = new Locale("da", "DK");
|
||||
Locale denmark = new Locale.Builder().setLanguage("da").setRegion("DK").build();
|
||||
assertNotNull(session.get(I18nInterceptor.DEFAULT_SESSION_ATTRIBUTE)); // should be stored here
|
||||
assertEquals(denmark, session.get(I18nInterceptor.DEFAULT_SESSION_ATTRIBUTE)); // should create a locale object
|
||||
}
|
||||
@@ -111,7 +112,7 @@ public class I18nInterceptorTest extends TestCase {
|
||||
|
||||
assertFalse(mai.getInvocationContext().getParameters().get(I18nInterceptor.DEFAULT_PARAMETER).isDefined()); // should have been removed
|
||||
|
||||
Locale denmark = new Locale("da", "DK");
|
||||
Locale denmark = new Locale.Builder().setLanguage("da").setRegion("DK").build();
|
||||
assertNull(session.get(I18nInterceptor.DEFAULT_SESSION_ATTRIBUTE)); // should be stored here
|
||||
assertEquals(denmark, mai.getInvocationContext().getLocale()); // should create a locale object
|
||||
}
|
||||
@@ -122,7 +123,7 @@ public class I18nInterceptorTest extends TestCase {
|
||||
|
||||
assertFalse(mai.getInvocationContext().getParameters().get(I18nInterceptor.DEFAULT_PARAMETER).isDefined()); // should have been removed
|
||||
|
||||
Locale denmark = new Locale("da");
|
||||
Locale denmark = Locale.forLanguageTag("da");
|
||||
assertNotNull(session.get(I18nInterceptor.DEFAULT_SESSION_ATTRIBUTE)); // should be stored here
|
||||
assertEquals(denmark, session.get(I18nInterceptor.DEFAULT_SESSION_ATTRIBUTE)); // should create a locale object
|
||||
}
|
||||
@@ -173,7 +174,7 @@ public class I18nInterceptorTest extends TestCase {
|
||||
|
||||
assertFalse(mai.getInvocationContext().getParameters().get(I18nInterceptor.DEFAULT_PARAMETER).isDefined()); // should have been removed
|
||||
|
||||
Locale variant = new Locale("ja", "JP", "JP");
|
||||
Locale variant = Locale.forLanguageTag("ja-JP-x-lvariant-JP");
|
||||
Locale locale = (Locale) session.get(I18nInterceptor.DEFAULT_SESSION_ATTRIBUTE);
|
||||
assertNotNull(locale); // should be stored here
|
||||
assertEquals(variant, locale);
|
||||
@@ -187,7 +188,7 @@ public class I18nInterceptorTest extends TestCase {
|
||||
assertFalse(mai.getInvocationContext().getParameters().get(I18nInterceptor.DEFAULT_PARAMETER).isDefined()); // should have been removed
|
||||
assertNull(session.get(I18nInterceptor.DEFAULT_SESSION_ATTRIBUTE));
|
||||
|
||||
Locale variant = new Locale("ja", "JP", "JP");
|
||||
Locale variant = Locale.forLanguageTag("ja-JP-x-lvariant-JP");
|
||||
Locale locale = mai.getInvocationContext().getLocale();
|
||||
assertNotNull(locale); // should be stored here
|
||||
assertEquals(variant, locale);
|
||||
@@ -205,7 +206,7 @@ public class I18nInterceptorTest extends TestCase {
|
||||
}
|
||||
|
||||
public void testRealLocalesInParams() throws Exception {
|
||||
Locale[] locales = new Locale[] { Locale.CANADA_FRENCH };
|
||||
Locale[] locales = new Locale[]{Locale.CANADA_FRENCH};
|
||||
assertTrue(locales.getClass().isArray());
|
||||
prepare(I18nInterceptor.DEFAULT_PARAMETER, locales);
|
||||
interceptor.intercept(mai);
|
||||
@@ -265,7 +266,7 @@ public class I18nInterceptorTest extends TestCase {
|
||||
|
||||
public void testAcceptLanguageBasedLocale() throws Exception {
|
||||
// given
|
||||
request.setPreferredLocales(Arrays.asList(new Locale("da_DK"), new Locale("pl")));
|
||||
request.setPreferredLocales(Arrays.asList(Locale.forLanguageTag("da-DK"), Locale.forLanguageTag("pl")));
|
||||
interceptor.setLocaleStorage(null);
|
||||
interceptor.setSupportedLocale("en,pl");
|
||||
|
||||
@@ -275,12 +276,143 @@ public class I18nInterceptorTest extends TestCase {
|
||||
// then
|
||||
assertNull(session.get(I18nInterceptor.DEFAULT_SESSION_ATTRIBUTE)); // should not be stored here
|
||||
assertNull(session.get(I18nInterceptor.DEFAULT_SESSION_ATTRIBUTE)); // should not create a locale object
|
||||
assertEquals(new Locale("pl"), mai.getInvocationContext().getLocale());
|
||||
assertEquals(Locale.forLanguageTag("pl"), mai.getInvocationContext().getLocale());
|
||||
}
|
||||
|
||||
public void testSupportedLocaleWithRequestLocale() throws Exception {
|
||||
// given - supportedLocale configured + request_locale param with SESSION storage
|
||||
request.setPreferredLocales(Arrays.asList(Locale.ENGLISH));
|
||||
interceptor.setSupportedLocale("en,fr");
|
||||
prepare(I18nInterceptor.DEFAULT_PARAMETER, "fr");
|
||||
|
||||
// when
|
||||
interceptor.intercept(mai);
|
||||
|
||||
// then - request_locale wins over Accept-Language
|
||||
assertEquals(Locale.FRENCH, session.get(I18nInterceptor.DEFAULT_SESSION_ATTRIBUTE));
|
||||
assertEquals(Locale.FRENCH, mai.getInvocationContext().getLocale());
|
||||
}
|
||||
|
||||
public void testSupportedLocaleRejectsUnsupportedRequestLocale() throws Exception {
|
||||
// given - request_locale=es but supportedLocale="en,fr"
|
||||
request.setPreferredLocales(Arrays.asList(Locale.ENGLISH));
|
||||
interceptor.setSupportedLocale("en,fr");
|
||||
prepare(I18nInterceptor.DEFAULT_PARAMETER, "es");
|
||||
|
||||
// when
|
||||
interceptor.intercept(mai);
|
||||
|
||||
// then - es rejected, falls back to Accept-Language match (en)
|
||||
assertNull(session.get(I18nInterceptor.DEFAULT_SESSION_ATTRIBUTE));
|
||||
assertEquals(Locale.ENGLISH, mai.getInvocationContext().getLocale());
|
||||
}
|
||||
|
||||
public void testSupportedLocaleRevalidatesSessionLocale() throws Exception {
|
||||
// given - session has stored locale "de" but supportedLocale changed to "en,fr"
|
||||
session.put(I18nInterceptor.DEFAULT_SESSION_ATTRIBUTE, Locale.GERMAN);
|
||||
request.setPreferredLocales(Arrays.asList(Locale.FRENCH));
|
||||
interceptor.setSupportedLocale("en,fr");
|
||||
|
||||
// when
|
||||
interceptor.intercept(mai);
|
||||
|
||||
// then - stored "de" rejected, falls back to Accept-Language match (fr)
|
||||
assertEquals(Locale.FRENCH, mai.getInvocationContext().getLocale());
|
||||
}
|
||||
|
||||
public void testSupportedLocaleWithCookieStorage() throws Exception {
|
||||
// given - supportedLocale configured + request_cookie_locale param with COOKIE storage
|
||||
prepare(I18nInterceptor.DEFAULT_COOKIE_PARAMETER, "fr");
|
||||
request.setPreferredLocales(Arrays.asList(Locale.ENGLISH));
|
||||
interceptor.setSupportedLocale("en,fr");
|
||||
|
||||
final Cookie cookie = new Cookie(I18nInterceptor.DEFAULT_COOKIE_ATTRIBUTE, "fr");
|
||||
HttpServletResponse response = EasyMock.createMock(HttpServletResponse.class);
|
||||
response.addCookie(CookieMatcher.eqCookie(cookie));
|
||||
EasyMock.replay(response);
|
||||
|
||||
ac.put(StrutsStatics.HTTP_RESPONSE, response);
|
||||
interceptor.setLocaleStorage(I18nInterceptor.Storage.COOKIE.name());
|
||||
|
||||
// when
|
||||
interceptor.intercept(mai);
|
||||
|
||||
// then - request_cookie_locale=fr wins
|
||||
EasyMock.verify(response);
|
||||
assertEquals(Locale.FRENCH, mai.getInvocationContext().getLocale());
|
||||
}
|
||||
|
||||
public void testSupportedLocaleRejectsUnsupportedRequestCookieLocale() throws Exception {
|
||||
// given - request_cookie_locale=es but supportedLocale="en,fr"
|
||||
prepare(I18nInterceptor.DEFAULT_COOKIE_PARAMETER, "es");
|
||||
request.setPreferredLocales(Arrays.asList(Locale.ENGLISH));
|
||||
interceptor.setSupportedLocale("en,fr");
|
||||
|
||||
HttpServletResponse response = EasyMock.createStrictMock(HttpServletResponse.class);
|
||||
EasyMock.replay(response);
|
||||
|
||||
ac.put(StrutsStatics.HTTP_RESPONSE, response);
|
||||
interceptor.setLocaleStorage(I18nInterceptor.Storage.COOKIE.name());
|
||||
|
||||
// when
|
||||
interceptor.intercept(mai);
|
||||
|
||||
// then - unsupported request_cookie_locale ignored, falls back to Accept-Language match
|
||||
EasyMock.verify(response);
|
||||
assertEquals(Locale.ENGLISH, mai.getInvocationContext().getLocale());
|
||||
}
|
||||
|
||||
public void testSupportedLocaleRevalidatesStoredCookieLocale() throws Exception {
|
||||
// given - cookie has stored "de" but supportedLocale changed to "en,fr"
|
||||
request.setCookies(new Cookie(I18nInterceptor.DEFAULT_COOKIE_ATTRIBUTE, "de"));
|
||||
request.setPreferredLocales(Arrays.asList(Locale.ITALIAN));
|
||||
interceptor.setSupportedLocale("en,fr");
|
||||
|
||||
HttpServletResponse response = EasyMock.createStrictMock(HttpServletResponse.class);
|
||||
EasyMock.replay(response);
|
||||
|
||||
ac.put(StrutsStatics.HTTP_RESPONSE, response);
|
||||
interceptor.setLocaleStorage(I18nInterceptor.Storage.COOKIE.name());
|
||||
|
||||
// when
|
||||
interceptor.intercept(mai);
|
||||
|
||||
// then - stored "de" rejected and fallback locale from invocation context is used
|
||||
EasyMock.verify(response);
|
||||
assertEquals(Locale.US, mai.getInvocationContext().getLocale());
|
||||
}
|
||||
|
||||
public void testRequestOnlyLocalePrecedenceWithSupportedLocale() throws Exception {
|
||||
// given - request_only_locale should win over Accept-Language match
|
||||
prepare(I18nInterceptor.DEFAULT_REQUEST_ONLY_PARAMETER, "fr");
|
||||
request.setPreferredLocales(Arrays.asList(Locale.ENGLISH));
|
||||
interceptor.setSupportedLocale("en,fr");
|
||||
|
||||
// when
|
||||
interceptor.intercept(mai);
|
||||
|
||||
// then - request_only_locale applied and not persisted
|
||||
assertNull(session.get(I18nInterceptor.DEFAULT_SESSION_ATTRIBUTE));
|
||||
assertEquals(Locale.FRENCH, mai.getInvocationContext().getLocale());
|
||||
}
|
||||
|
||||
public void testSupportedLocaleRejectsUnsupportedRequestOnlyLocale() throws Exception {
|
||||
// given - request_only_locale=es but supportedLocale="en,fr"
|
||||
prepare(I18nInterceptor.DEFAULT_REQUEST_ONLY_PARAMETER, "es");
|
||||
request.setPreferredLocales(Arrays.asList(Locale.ENGLISH));
|
||||
interceptor.setSupportedLocale("en,fr");
|
||||
|
||||
// when
|
||||
interceptor.intercept(mai);
|
||||
|
||||
// then - es rejected, falls back to stored session locale / invocation context
|
||||
assertNull(session.get(I18nInterceptor.DEFAULT_SESSION_ATTRIBUTE));
|
||||
assertEquals(Locale.ENGLISH, mai.getInvocationContext().getLocale());
|
||||
}
|
||||
|
||||
public void testAcceptLanguageBasedLocaleWithFallbackToDefault() throws Exception {
|
||||
// given
|
||||
request.setPreferredLocales(Arrays.asList(new Locale("da_DK"), new Locale("es")));
|
||||
request.setPreferredLocales(Arrays.asList(Locale.forLanguageTag("da-DK"), Locale.forLanguageTag("es")));
|
||||
|
||||
interceptor.setLocaleStorage(null);
|
||||
interceptor.setSupportedLocale("en,pl");
|
||||
@@ -308,9 +440,9 @@ public class I18nInterceptorTest extends TestCase {
|
||||
session = new HashMap<>();
|
||||
|
||||
ac = ActionContext.of()
|
||||
.bind()
|
||||
.withSession(session)
|
||||
.withParameters(HttpParameters.create().build());
|
||||
.bind()
|
||||
.withSession(session)
|
||||
.withParameters(HttpParameters.create().build());
|
||||
|
||||
request = new MockHttpServletRequest();
|
||||
request.setSession(new MockHttpSession());
|
||||
@@ -348,8 +480,8 @@ public class I18nInterceptorTest extends TestCase {
|
||||
public boolean matches(Object argument) {
|
||||
Cookie cookie = ((Cookie) argument);
|
||||
return
|
||||
(cookie.getName().equals(expected.getName()) &&
|
||||
cookie.getValue().equals(expected.getValue()));
|
||||
(cookie.getName().equals(expected.getName()) &&
|
||||
cookie.getValue().equals(expected.getValue()));
|
||||
}
|
||||
|
||||
public static Cookie eqCookie(Cookie ck) {
|
||||
@@ -359,10 +491,10 @@ public class I18nInterceptorTest extends TestCase {
|
||||
|
||||
public void appendTo(StringBuffer buffer) {
|
||||
buffer
|
||||
.append("Received")
|
||||
.append(expected.getName())
|
||||
.append("/")
|
||||
.append(expected.getValue());
|
||||
.append("Received")
|
||||
.append(expected.getName())
|
||||
.append("/")
|
||||
.append(expected.getValue());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+48
@@ -217,6 +217,54 @@ public class HttpMethodInterceptorTest extends StrutsInternalTestCase {
|
||||
assertEquals(HttpMethod.POST, action.getHttpMethod());
|
||||
}
|
||||
|
||||
/**
|
||||
* Simulates a wildcard action like {@code <action name="Wild-*" method="{1}">}
|
||||
* resolving to method "onPostOnly" (annotated with @HttpPost).
|
||||
* With the fix in DefaultActionProxy.resolveMethod(), config-resolved methods
|
||||
* set isMethodSpecified()=true, so the interceptor checks method-level annotations.
|
||||
* A GET request should be rejected because @HttpPost only allows POST.
|
||||
*/
|
||||
public void testWildcardResolvedMethodWithPostAnnotationRejectsGet() throws Exception {
|
||||
// given
|
||||
HttpMethodsTestAction action = new HttpMethodsTestAction();
|
||||
prepareActionInvocation(action);
|
||||
// Simulate wildcard resolution: Wild-onPostOnly -> method="onPostOnly"
|
||||
actionProxy.setMethod("onPostOnly");
|
||||
// After the fix, config-resolved methods have methodSpecified=true
|
||||
actionProxy.setMethodSpecified(true);
|
||||
|
||||
invocation.setResultCode("onPostOnly");
|
||||
|
||||
prepareRequest("get");
|
||||
|
||||
// when
|
||||
String resultName = interceptor.intercept(invocation);
|
||||
|
||||
// then - interceptor checks method-level @HttpPost and rejects GET
|
||||
assertEquals("bad-request", resultName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Counterpart: same wildcard scenario but with POST request — should succeed.
|
||||
*/
|
||||
public void testWildcardResolvedMethodWithPostAnnotationAllowsPost() throws Exception {
|
||||
// given
|
||||
HttpMethodsTestAction action = new HttpMethodsTestAction();
|
||||
prepareActionInvocation(action);
|
||||
actionProxy.setMethod("onPostOnly");
|
||||
actionProxy.setMethodSpecified(true);
|
||||
|
||||
invocation.setResultCode("onPostOnly");
|
||||
|
||||
prepareRequest("post");
|
||||
|
||||
// when
|
||||
String resultName = interceptor.intercept(invocation);
|
||||
|
||||
// then - interceptor checks method-level @HttpPost and allows POST
|
||||
assertEquals("onPostOnly", resultName);
|
||||
}
|
||||
|
||||
private void prepareActionInvocation(Object action) {
|
||||
interceptor = new HttpMethodInterceptor();
|
||||
invocation = new MockActionInvocation();
|
||||
|
||||
+2
-2
@@ -42,7 +42,7 @@ import org.apache.struts2.ognl.accessor.RootAccessor;
|
||||
import org.apache.struts2.util.ValueStack;
|
||||
import org.apache.struts2.util.ValueStackFactory;
|
||||
import org.apache.struts2.util.reflection.ReflectionContextState;
|
||||
import ognl.OgnlContext;
|
||||
import org.apache.struts2.ognl.StrutsContext;
|
||||
import org.apache.struts2.action.NoParameters;
|
||||
import org.apache.struts2.action.ParameterNameAware;
|
||||
import org.apache.struts2.action.ParameterValueAware;
|
||||
@@ -353,7 +353,7 @@ public class ParametersInterceptorTest extends XWorkTestCase {
|
||||
//then
|
||||
assertEquals("This is blah", ((SimpleAction) proxy.getAction()).getBlah());
|
||||
Field field = ReflectionContextState.class.getField("DENY_METHOD_EXECUTION");
|
||||
boolean allowStaticFieldAccess = ((OgnlContext) stack.getContext()).getMemberAccess().isAccessible((OgnlContext) stack.getContext(), ReflectionContextState.class, field, "");
|
||||
boolean allowStaticFieldAccess = ((StrutsContext) stack.getContext()).getMemberAccess().isAccessible((StrutsContext) stack.getContext(), ReflectionContextState.class, field, "");
|
||||
assertFalse(allowStaticFieldAccess);
|
||||
}
|
||||
|
||||
|
||||
+5
@@ -30,7 +30,9 @@ import org.apache.struts2.ognl.DefaultOgnlBeanInfoCacheFactory;
|
||||
import org.apache.struts2.ognl.DefaultOgnlExpressionCacheFactory;
|
||||
import org.apache.struts2.ognl.OgnlUtil;
|
||||
import org.apache.struts2.ognl.StrutsOgnlGuard;
|
||||
import org.apache.struts2.ognl.StrutsProxyCacheFactory;
|
||||
import org.apache.struts2.ognl.ThreadAllowlist;
|
||||
import org.apache.struts2.util.StrutsProxyService;
|
||||
import org.apache.struts2.security.AcceptedPatternsChecker.IsAccepted;
|
||||
import org.apache.struts2.security.ExcludedPatternsChecker.IsExcluded;
|
||||
import org.apache.struts2.security.NotExcludedAcceptedPatternsChecker;
|
||||
@@ -71,6 +73,9 @@ public class StrutsParameterAnnotationTest {
|
||||
new StrutsOgnlGuard());
|
||||
parametersInterceptor.setOgnlUtil(ognlUtil);
|
||||
|
||||
var proxyService = new StrutsProxyService(new StrutsProxyCacheFactory<>("1000", "basic"));
|
||||
parametersInterceptor.setProxyService(proxyService);
|
||||
|
||||
NotExcludedAcceptedPatternsChecker checker = mock(NotExcludedAcceptedPatternsChecker.class);
|
||||
when(checker.isAccepted(anyString())).thenReturn(IsAccepted.yes(""));
|
||||
when(checker.isExcluded(anyString())).thenReturn(IsExcluded.no(Set.of()));
|
||||
|
||||
@@ -23,7 +23,6 @@ import ognl.MethodFailedException;
|
||||
import ognl.NoSuchPropertyException;
|
||||
import ognl.NullHandler;
|
||||
import ognl.Ognl;
|
||||
import ognl.OgnlContext;
|
||||
import ognl.OgnlException;
|
||||
import ognl.OgnlRuntime;
|
||||
import ognl.SimpleNode;
|
||||
@@ -90,12 +89,12 @@ public class OgnlUtilTest extends XWorkTestCase {
|
||||
public void testCanSetADependentObject() {
|
||||
String dogName = "fido";
|
||||
|
||||
OgnlRuntime.setNullHandler(Owner.class, new NullHandler() {
|
||||
public Object nullMethodResult(OgnlContext context, Object o, String s, Object[] objects) {
|
||||
OgnlRuntime.setNullHandler(Owner.class, new NullHandler<StrutsContext>() {
|
||||
public Object nullMethodResult(StrutsContext context, Object o, String s, Object[] objects) {
|
||||
return null;
|
||||
}
|
||||
|
||||
public Object nullPropertyValue(OgnlContext context, Object o, Object o1) {
|
||||
public Object nullPropertyValue(StrutsContext context, Object o, Object o1) {
|
||||
String methodName = o1.toString();
|
||||
String getter = "set" + methodName.substring(0, 1).toUpperCase() + methodName.substring(1);
|
||||
Method[] methods = o.getClass().getDeclaredMethods();
|
||||
@@ -199,7 +198,7 @@ public class OgnlUtilTest extends XWorkTestCase {
|
||||
|
||||
public void testExpressionIsCachedIrrespectiveOfItsExecutionStatus() {
|
||||
Foo foo = new Foo();
|
||||
OgnlContext context = ognlUtil.createDefaultContext(foo);
|
||||
StrutsContext context = ognlUtil.createDefaultContext(foo);
|
||||
|
||||
// Expression which executes with success
|
||||
try {
|
||||
@@ -223,7 +222,7 @@ public class OgnlUtilTest extends XWorkTestCase {
|
||||
ognlUtil.setContainer(container); // Must be explicitly set as the generated OgnlUtil instance has no container
|
||||
ognlUtil.setEnableExpressionCache("true");
|
||||
Foo foo = new Foo();
|
||||
OgnlContext context = ognlUtil.createDefaultContext(foo);
|
||||
StrutsContext context = ognlUtil.createDefaultContext(foo);
|
||||
|
||||
// Expression which executes with success
|
||||
try {
|
||||
@@ -243,7 +242,7 @@ public class OgnlUtilTest extends XWorkTestCase {
|
||||
|
||||
public void testMethodExpressionIsCachedIrrespectiveOfItsExecutionStatus() {
|
||||
Foo foo = new Foo();
|
||||
OgnlContext context = ognlUtil.createDefaultContext(foo);
|
||||
StrutsContext context = ognlUtil.createDefaultContext(foo);
|
||||
|
||||
// Method expression which executes with success
|
||||
try {
|
||||
@@ -846,7 +845,7 @@ public class OgnlUtilTest extends XWorkTestCase {
|
||||
ChainingInterceptor foo = new ChainingInterceptor();
|
||||
ChainingInterceptor foo2 = new ChainingInterceptor();
|
||||
|
||||
OgnlContext context = ognlUtil.createDefaultContext(null);
|
||||
StrutsContext context = ognlUtil.createDefaultContext(null);
|
||||
SimpleNode expression = (SimpleNode) Ognl.parseExpression("{'a','ruby','b','tom'}");
|
||||
|
||||
Ognl.getValue(expression, context, "aksdj");
|
||||
@@ -903,7 +902,7 @@ public class OgnlUtilTest extends XWorkTestCase {
|
||||
public void testBeanMapExpressions() throws OgnlException, NoSuchMethodException {
|
||||
Foo foo = new Foo();
|
||||
|
||||
OgnlContext context = ognlUtil.createDefaultContext(foo);
|
||||
StrutsContext context = ognlUtil.createDefaultContext(foo);
|
||||
SecurityMemberAccess sma = (SecurityMemberAccess) context.getMemberAccess();
|
||||
|
||||
sma.useExcludedPackageNames("org.apache.struts2.ognl");
|
||||
|
||||
@@ -19,12 +19,13 @@
|
||||
package org.apache.struts2.ognl;
|
||||
|
||||
import ognl.MemberAccess;
|
||||
import ognl.OgnlContext;
|
||||
import org.apache.commons.lang3.reflect.FieldUtils;
|
||||
import org.apache.struts2.TestBean;
|
||||
import org.apache.struts2.config.ConfigurationException;
|
||||
import org.apache.struts2.test.TestBean2;
|
||||
import org.apache.struts2.util.StrutsProxyService;
|
||||
import org.apache.struts2.util.Foo;
|
||||
import org.apache.struts2.util.ProxyService;
|
||||
import org.hibernate.proxy.HibernateProxy;
|
||||
import org.hibernate.proxy.LazyInitializer;
|
||||
import org.junit.Before;
|
||||
@@ -53,19 +54,21 @@ import static org.mockito.Mockito.when;
|
||||
|
||||
public class SecurityMemberAccessTest {
|
||||
|
||||
private OgnlContext context;
|
||||
private StrutsContext context;
|
||||
private FooBar target;
|
||||
protected SecurityMemberAccess sma;
|
||||
protected ProviderAllowlist mockedProviderAllowlist;
|
||||
protected ThreadAllowlist mockedThreadAllowlist;
|
||||
protected ProxyService proxyService;
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
context = ognl.Ognl.createDefaultContext(null);
|
||||
target = new FooBar();
|
||||
mockedProviderAllowlist = mock(ProviderAllowlist.class);
|
||||
mockedThreadAllowlist = mock(ThreadAllowlist.class);
|
||||
proxyService = new StrutsProxyService(new StrutsProxyCacheFactory<>("1000", "basic"));
|
||||
assignNewSma(true);
|
||||
context = new StrutsContext(sma);
|
||||
}
|
||||
|
||||
protected void assignNewSma(boolean allowStaticFieldAccess) {
|
||||
@@ -77,6 +80,7 @@ public class SecurityMemberAccessTest {
|
||||
|
||||
protected void assignNewSmaHelper() {
|
||||
sma = new SecurityMemberAccess(mockedProviderAllowlist, mockedThreadAllowlist);
|
||||
sma.setProxyService(proxyService);
|
||||
}
|
||||
|
||||
private <T> T reflectField(String fieldName) throws IllegalAccessException {
|
||||
|
||||
@@ -37,7 +37,6 @@ import org.apache.struts2.util.ValueStack;
|
||||
import org.apache.struts2.util.location.LocatableProperties;
|
||||
import org.apache.struts2.util.reflection.ReflectionContextState;
|
||||
import ognl.Ognl;
|
||||
import ognl.OgnlContext;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
@@ -57,7 +56,8 @@ public class SetPropertiesTest extends XWorkTestCase {
|
||||
|
||||
public void testOgnlUtilEmptyStringAsLong() {
|
||||
Bar bar = new Bar();
|
||||
OgnlContext context = Ognl.createDefaultContext(bar, new SecurityMemberAccess(null, null));
|
||||
StrutsContext context = new StrutsContext(new SecurityMemberAccess(null, null));
|
||||
context.withRoot(bar);
|
||||
context.put(XWorkConverter.REPORT_CONVERSION_ERRORS, Boolean.TRUE);
|
||||
bar.setId(null);
|
||||
|
||||
@@ -81,7 +81,7 @@ public class SetPropertiesTest extends XWorkTestCase {
|
||||
ValueStack vs = ActionContext.getContext().getValueStack();
|
||||
vs.getContext().put(XWorkConverter.REPORT_CONVERSION_ERRORS, Boolean.TRUE);
|
||||
|
||||
XWorkConverter c = (XWorkConverter) ((OgnlTypeConverterWrapper) Ognl.getTypeConverter((OgnlContext) vs.getContext())).getTarget();
|
||||
XWorkConverter c = (XWorkConverter) ((OgnlTypeConverterWrapper) ((StrutsContext) vs.getContext()).getTypeConverter()).getTarget();
|
||||
c.registerConverter(Cat.class.getName(), new FooBarConverter());
|
||||
vs.push(foo);
|
||||
|
||||
@@ -97,7 +97,7 @@ public class SetPropertiesTest extends XWorkTestCase {
|
||||
ValueStack vs = ActionContext.getContext().getValueStack();
|
||||
vs.getContext().put(XWorkConverter.REPORT_CONVERSION_ERRORS, Boolean.TRUE);
|
||||
|
||||
XWorkConverter c = (XWorkConverter) ((OgnlTypeConverterWrapper) Ognl.getTypeConverter((OgnlContext) vs.getContext())).getTarget();
|
||||
XWorkConverter c = (XWorkConverter) ((OgnlTypeConverterWrapper) ((StrutsContext) vs.getContext()).getTypeConverter()).getTarget();
|
||||
c.registerConverter(Cat.class.getName(), new FooBarConverter());
|
||||
vs.push(foo);
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user