mirror of
https://github.com/apache/struts.git
synced 2026-08-09 00:27:04 +00:00
Compare commits
160 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 02858b7ed5 | |||
| 8fcab78c5d | |||
| d50cfba32e | |||
| 1b43b53c6b | |||
| 5e5795559b | |||
| 9f424a94f7 | |||
| 32a763b958 | |||
| 66b2dc0117 | |||
| 11c83d9484 | |||
| a9d804ebed | |||
| 80eacb3400 | |||
| e685523d19 | |||
| 88d828f216 | |||
| 75e84a25e9 | |||
| 2bd358ce1d | |||
| ef63030c86 | |||
| d11153e942 | |||
| f278f4bbdb | |||
| 4031ab0c01 | |||
| 3bade0c8ce | |||
| 69cf6551e5 | |||
| 5443805ac1 | |||
| a599f0b6ed | |||
| 4d375cc0bc | |||
| ae58d5cea2 | |||
| dcb58e1b73 | |||
| e15ed665b2 | |||
| fd469c8c3b | |||
| 28b33b3210 | |||
| 4f4b4ec8a6 | |||
| 8e5c692d36 | |||
| f0c4545f41 | |||
| d305c1d3df | |||
| d4bce051ed | |||
| 16525f0ce2 | |||
| ccaa61431a | |||
| c3877c2cf0 | |||
| cf215315e0 | |||
| 1bc3ebcb73 | |||
| b553129914 | |||
| 687f762507 | |||
| 79c6bf94b3 | |||
| 0bc1a4391c | |||
| 0ecd95a16b | |||
| 014c2bd5dd | |||
| 1599d5dab5 | |||
| 3085ab2894 | |||
| ed55bfc0be | |||
| e912492edb | |||
| 3507422559 | |||
| ffd699682d | |||
| e9f98f02ec | |||
| e1e7c41344 | |||
| 3f138e3c0a | |||
| f30cf63d43 | |||
| d721f3fb65 | |||
| aaa4984eac | |||
| fd82d6354f | |||
| dd68723a47 | |||
| fa78ec43a8 | |||
| 1579e62e8a | |||
| 5238e9c1cc | |||
| f16abd171b | |||
| c8ab33e3ce | |||
| 37144a03f7 | |||
| 824e7121b0 | |||
| 63f2c8bdec | |||
| 359b6549ef | |||
| 201b9e860e | |||
| a1c4cb60a2 | |||
| b796bab432 | |||
| 7d19767f71 | |||
| 436ce3560b | |||
| 1d6b05b829 | |||
| 2487d583f3 | |||
| 3c5c7e5b7b | |||
| 7fef6c1ee0 | |||
| 73e05ef978 | |||
| 3a6ee8aacc | |||
| a654da4db3 | |||
| de0b6f13b6 | |||
| f640012618 | |||
| be3d57a725 | |||
| ae178a0190 | |||
| d3812548b7 | |||
| 85a4be3402 | |||
| 5b420899f5 | |||
| 0c45954dcd | |||
| 00c26a7a19 | |||
| cc4d05f6bf | |||
| bf9d1548ea | |||
| b87ec9804c | |||
| 530f3157f9 | |||
| 6b9e6998b0 | |||
| e8e8e66580 | |||
| 12814e7a7f | |||
| 2bfa4b6335 | |||
| 607401c2d6 | |||
| 281aa0004f | |||
| 033b55c13d | |||
| e043d41451 | |||
| 6b80a1cf50 | |||
| b70f2aed1f | |||
| 6466bb120d | |||
| e2f6bc287e | |||
| c57a989584 | |||
| 548c70a021 | |||
| 66f8e0fe0a | |||
| 8124e6cfa4 | |||
| 5b7991412b | |||
| 90692cf919 | |||
| f06bc517ab | |||
| d27d3fba3a | |||
| f66e078f10 | |||
| 1331e99742 | |||
| 9c40e2d0df | |||
| a7d915d627 | |||
| d9c9d2b030 | |||
| d4f1adbb05 | |||
| 1ab2fcc620 | |||
| b22022c25f | |||
| 101dc02f77 | |||
| 84a97741ed | |||
| e326799532 | |||
| 285d2d3681 | |||
| b650adb539 | |||
| 993620eb67 | |||
| d564829a82 | |||
| eb4a5a0086 | |||
| bcb3d91293 | |||
| 6f753fb8d9 | |||
| ab55fda4ae | |||
| 33bef8e665 | |||
| d47143c689 | |||
| 96f838df1e | |||
| 7ae52ccfcc | |||
| 1c3219e7f9 | |||
| 9861b834ab | |||
| 747859b72a | |||
| 433c4837fd | |||
| 4cc874d426 | |||
| f35c808efc | |||
| 6bb71caa90 | |||
| aeaa4f26cf | |||
| 117b9c741b | |||
| e09572f3b0 | |||
| 8579a10f59 | |||
| 0d2d11071b | |||
| 1100a34e9d | |||
| fa951718dc | |||
| 2ab0a3c843 | |||
| 15ee2ac4a0 | |||
| d2cb444bcf | |||
| 84cf666041 | |||
| 31cb558fc6 | |||
| 8b66b575a1 | |||
| 077e985899 | |||
| 5c22c0da74 | |||
| e9116ae3da | |||
| d727fbf6be |
@@ -17,16 +17,18 @@ github:
|
||||
protected_branches:
|
||||
main:
|
||||
# contexts are the names of checks that must pass.
|
||||
contexts:
|
||||
- build
|
||||
required_status_checks:
|
||||
contexts:
|
||||
- "Build and Test (JDK 17)"
|
||||
required_pull_request_reviews:
|
||||
# it does not work because our github teams are private/secret, see INFRA-25666
|
||||
require_code_owner_reviews: false
|
||||
required_approving_review_count: 0
|
||||
release/struts-6-7-x:
|
||||
release/*:
|
||||
# contexts are the names of checks that must pass.
|
||||
contexts:
|
||||
- build
|
||||
required_status_checks:
|
||||
contexts:
|
||||
- "Build and Test (JDK 8)"
|
||||
required_pull_request_reviews:
|
||||
# it does not work because our github teams are private/secret, see INFRA-25666
|
||||
require_code_owner_reviews: false
|
||||
|
||||
@@ -8,4 +8,9 @@ updates:
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
ignore: []
|
||||
target-branch: "main"
|
||||
- package-ecosystem: "maven"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
target-branch: "release/struts-6-7-x"
|
||||
|
||||
@@ -44,20 +44,20 @@ jobs:
|
||||
language: [ 'java' ]
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
- name: Setup Java JDK
|
||||
uses: actions/setup-java@v4
|
||||
uses: actions/setup-java@v5
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: 17
|
||||
cache: 'maven'
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v3.28.9
|
||||
uses: github/codeql-action/init@v3.30.3
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@v3.28.9
|
||||
uses: github/codeql-action/autobuild@v3.30.3
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v3.28.9
|
||||
uses: github/codeql-action/analyze@v3.30.3
|
||||
with:
|
||||
category: "/language:${{matrix.language}}"
|
||||
|
||||
@@ -30,18 +30,25 @@ env:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build and Test
|
||||
name: Build and Test (JDK ${{ matrix.java }})${{ matrix.profile == '-Pjakartaee11' && ' with Jakarta EE 11' || matrix.profile }}
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
java: [ '17', '21' ]
|
||||
include:
|
||||
- java: '17'
|
||||
profile: ''
|
||||
- java: '21'
|
||||
profile: ''
|
||||
- java: '21'
|
||||
profile: '-Pjakartaee11'
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
- uses: actions/setup-java@v4
|
||||
uses: actions/checkout@v5
|
||||
- name: Setup Java ${{ matrix.java }}
|
||||
uses: actions/setup-java@v5
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: ${{ matrix.java }}
|
||||
cache: 'maven'
|
||||
- name: Build with Maven on Java ${{ matrix.java }}
|
||||
run: mvn -B -V -DskipAssembly verify --no-transfer-progress
|
||||
- name: Maven Verify on Java ${{ matrix.java }}${{ matrix.profile == '-Pjakartaee11' && ' (Jakarta EE 11)' || matrix.profile }}
|
||||
run: mvn -B -V -DskipAssembly verify ${{ matrix.profile }} --no-transfer-progress
|
||||
|
||||
@@ -41,12 +41,12 @@ jobs:
|
||||
steps:
|
||||
|
||||
- name: "Checkout code"
|
||||
uses: actions/checkout@v4 # 3.1.0
|
||||
uses: actions/checkout@v5 # 3.1.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: "Run analysis"
|
||||
uses: ossf/scorecard-action@62b2cac7ed8198b15735ed49ab1e5cf35480ba46 # 2.4.0
|
||||
uses: ossf/scorecard-action@05b42c624433fc40578a4040d5cf5e36ddca8cde # 2.4.2
|
||||
with:
|
||||
results_file: results.sarif
|
||||
results_format: sarif
|
||||
@@ -58,13 +58,13 @@ jobs:
|
||||
publish_results: true
|
||||
|
||||
- name: "Upload artifact"
|
||||
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # 4.6.0
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # 4.6.2
|
||||
with:
|
||||
name: SARIF file
|
||||
path: results.sarif
|
||||
retention-days: 5
|
||||
|
||||
- name: "Upload to code-scanning"
|
||||
uses: github/codeql-action/upload-sarif@0a35e8f6866a39b001e5f7ad1d0daf9836786896 # 2.22.11
|
||||
uses: github/codeql-action/upload-sarif@aa90e97ad2ed17cde6a43e89f70138299e64f837 # 2.22.11
|
||||
with:
|
||||
sarif_file: results.sarif
|
||||
|
||||
@@ -33,10 +33,10 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ !github.event.pull_request.base.repo.fork && !github.event.pull_request.head.repo.fork && github.actor != 'dependabot[bot]' }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- uses: actions/setup-java@v4
|
||||
- uses: actions/setup-java@v5
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: 21
|
||||
|
||||
@@ -46,3 +46,6 @@ test-output
|
||||
|
||||
# Tidelift CLI scanner
|
||||
.tidelift
|
||||
|
||||
# Claude Code local settings
|
||||
.claude/
|
||||
@@ -0,0 +1,210 @@
|
||||
# Claude Code Best Practices for Apache Struts
|
||||
|
||||
This document outlines essential practices for working with Claude Code on the Apache Struts project, based on security improvements and testing implementations.
|
||||
|
||||
## Project Context
|
||||
- **Framework**: Apache Struts 2 (Java-based web framework)
|
||||
- **Technology Stack**: Jakarta EE, Maven, Java 17
|
||||
- **Key Libraries**: OGNL, Commons FileUpload2, Log4j2, JUnit, AssertJ
|
||||
- **Build System**: Maven with standard lifecycle
|
||||
|
||||
## Security-First Development
|
||||
|
||||
### Critical Security Principles
|
||||
1. **Never create files in system temp directories** - always use controlled application directories
|
||||
2. **Use UUID-based naming** for temporary files to prevent collisions and path traversal
|
||||
3. **Implement proper resource cleanup** with try-with-resources and finally blocks
|
||||
4. **Track all temporary resources** for explicit cleanup (security critical)
|
||||
5. **Validate all user inputs** and sanitize filenames before processing
|
||||
|
||||
### Security Implementation Patterns
|
||||
```java
|
||||
// GOOD: Secure temporary file creation
|
||||
protected File createTemporaryFile(String fileName, Path location) {
|
||||
String uid = UUID.randomUUID().toString().replace("-", "_");
|
||||
File file = location.resolve("upload_" + uid + ".tmp").toFile();
|
||||
LOG.debug("Creating temporary file: {} (originally: {})", file.getName(), fileName);
|
||||
return file;
|
||||
}
|
||||
|
||||
// BAD: Insecure system temp usage
|
||||
File tempFile = File.createTempFile("struts_upload_", "_" + item.getName());
|
||||
```
|
||||
|
||||
### Resource Management
|
||||
- Always use tracking collections for cleanup: `List<File> temporaryFiles`, `List<DiskFileItem> diskFileItems`
|
||||
- Implement protected cleanup methods for extensibility
|
||||
- Make cleanup idempotent and exception-safe
|
||||
- Use try-with-resources for streams and I/O operations
|
||||
|
||||
## Testing Implementation
|
||||
|
||||
### Test Structure & Coverage
|
||||
- **Unit Tests**: Test individual methods with mocked dependencies
|
||||
- **Integration Tests**: Test complete workflows with real file I/O
|
||||
- **Security Tests**: Verify directory traversal prevention, secure naming
|
||||
- **Error Handling Tests**: Test exception scenarios and error reporting
|
||||
- **Cleanup Tests**: Verify resource cleanup and tracking
|
||||
|
||||
### Testing Commands
|
||||
```bash
|
||||
# Run all tests
|
||||
mvn test -DskipAssembly
|
||||
|
||||
# Run specific test class
|
||||
mvn test -Dtest=JakartaMultiPartRequestTest
|
||||
|
||||
# Run tests with specific method pattern
|
||||
mvn test -Dtest=*MultiPartRequestTest#temporal*
|
||||
```
|
||||
|
||||
use `-DskipAssembly` to avoid building zip files with docs, examples, etc.
|
||||
|
||||
### Test Implementation Patterns
|
||||
```java
|
||||
@Test
|
||||
public void securityTestExample() throws Exception {
|
||||
// given - malicious input
|
||||
String maliciousFilename = "malicious../../../etc/passwd";
|
||||
|
||||
// when - process input
|
||||
processFile(maliciousFilename);
|
||||
|
||||
// then - verify security measures
|
||||
assertThat(tempFile.getParent()).isEqualTo(saveDir);
|
||||
assertThat(tempFile.getName()).doesNotContain("..");
|
||||
}
|
||||
```
|
||||
|
||||
### Reflection-Based Testing for Private Members
|
||||
```java
|
||||
Field privateField = ClassName.class.getDeclaredField("fieldName");
|
||||
privateField.setAccessible(true);
|
||||
@SuppressWarnings("unchecked")
|
||||
List<Type> values = (List<Type>) privateField.get(instance);
|
||||
```
|
||||
|
||||
## JavaDoc Documentation Standards
|
||||
|
||||
### Class-Level Documentation
|
||||
```java
|
||||
/**
|
||||
* Brief description of the class purpose and functionality.
|
||||
*
|
||||
* <p>Detailed description with multiple paragraphs explaining:</p>
|
||||
* <ul>
|
||||
* <li>Key features and capabilities</li>
|
||||
* <li>Security considerations</li>
|
||||
* <li>Resource management approach</li>
|
||||
* <li>Usage patterns and examples</li>
|
||||
* </ul>
|
||||
*
|
||||
* <p>Usage example:</p>
|
||||
* <pre>
|
||||
* ClassName instance = new ClassName();
|
||||
* try {
|
||||
* instance.process(data);
|
||||
* } finally {
|
||||
* instance.cleanUp(); // Always clean up resources
|
||||
* }
|
||||
* </pre>
|
||||
*
|
||||
* @see RelatedClass
|
||||
* @see org.apache.package.ImportantInterface
|
||||
*/
|
||||
```
|
||||
|
||||
### Method-Level Documentation
|
||||
```java
|
||||
/**
|
||||
* Brief description of what the method does.
|
||||
*
|
||||
* <p>Detailed description explaining:</p>
|
||||
* <ol>
|
||||
* <li>Step-by-step process</li>
|
||||
* <li>Security considerations</li>
|
||||
* <li>Error handling behavior</li>
|
||||
* <li>Resource management</li>
|
||||
* </ol>
|
||||
*
|
||||
* <p>Security note: This method creates files in controlled directory
|
||||
* to prevent security vulnerabilities.</p>
|
||||
*
|
||||
* @param paramName description of parameter and constraints
|
||||
* @param saveDir the directory where files will be created (must exist)
|
||||
* @return description of return value
|
||||
* @throws IOException if file creation fails or I/O error occurs
|
||||
* @see #relatedMethod(Type)
|
||||
* @see #cleanUpMethod()
|
||||
*/
|
||||
```
|
||||
|
||||
### Documentation Best Practices
|
||||
- **Always document security implications** in methods handling files/user input
|
||||
- **Include usage examples** for complex methods and classes
|
||||
- **Document exception conditions** and error handling behavior
|
||||
- **Reference related methods** using `@see` tags
|
||||
- **Explain resource management** responsibilities
|
||||
- **Use `<p>`, `<ol>`, `<ul>`, `<li>` for structured content
|
||||
- **Include `<pre>` blocks** for code examples
|
||||
|
||||
## Error Handling & Logging
|
||||
|
||||
### Error Message Patterns
|
||||
```java
|
||||
// Localized error messages
|
||||
LocalizedMessage errorMessage = buildErrorMessage(
|
||||
e.getClass(),
|
||||
e.getMessage(),
|
||||
new Object[]{fileName}
|
||||
);
|
||||
if (!errors.contains(errorMessage)) {
|
||||
errors.add(errorMessage);
|
||||
}
|
||||
```
|
||||
|
||||
### Logging Best Practices
|
||||
```java
|
||||
// Use parameterized logging for performance
|
||||
LOG.debug("Processing file: {} in directory: {}",
|
||||
normalizeSpace(fileName), saveDir);
|
||||
|
||||
// Log security-relevant operations
|
||||
LOG.warn("Failed to delete temporary file: {}", tempFile.getAbsolutePath());
|
||||
|
||||
// Use appropriate log levels
|
||||
LOG.debug() // Development details
|
||||
LOG.info() // General information
|
||||
LOG.warn() // Potential issues
|
||||
LOG.error() // Serious problems
|
||||
```
|
||||
|
||||
## Code Quality Standards
|
||||
|
||||
### Method Scope & Extensibility
|
||||
- Use `protected` for methods that subclasses might override
|
||||
- Implement cleanup methods as separate `protected` methods
|
||||
- Make core functionality extensible while maintaining security
|
||||
|
||||
### Exception Handling
|
||||
- Catch specific exceptions rather than generic `Exception`
|
||||
- Log exceptions with context but continue cleanup operations
|
||||
- Use try-finally blocks to ensure cleanup always occurs
|
||||
|
||||
### Code Organization
|
||||
- Group related methods together (processing, cleanup, utilities)
|
||||
- Keep security-critical code in dedicated methods
|
||||
- Use clear, descriptive method and variable names
|
||||
- Follow existing project conventions and patterns
|
||||
|
||||
## Common Pitfalls to Avoid
|
||||
|
||||
1. **File Security**: Never use `File.createTempFile()` without directory control
|
||||
2. **Resource Leaks**: Always track and clean up temporary files
|
||||
3. **Test Coverage**: Don't forget to test error conditions and cleanup
|
||||
4. **Documentation**: Always document security implications
|
||||
5. **Exception Handling**: Don't let cleanup failures affect main operations
|
||||
6. **Path Validation**: Always validate and sanitize file paths
|
||||
7. **Reflection Testing**: Use `@SuppressWarnings("unchecked")` appropriately
|
||||
|
||||
This document should be updated as new patterns and practices emerge during development.
|
||||
@@ -19,7 +19,7 @@ The Apache Struts web framework
|
||||
|
||||
[](https://ci-builds.apache.org/job/Struts/job/Struts%20Core/job/main/)
|
||||
[](https://github.com/apache/struts/actions/workflows/maven.yml)
|
||||
[](https://maven-badges.herokuapp.com/maven-central/org.apache.struts/struts2-core/)
|
||||
[](https://maven-badges.sml.io/maven-central/org.apache.struts/struts2-core/)
|
||||
[](https://javadoc.io/doc/org.apache.struts/struts2-core)
|
||||
[](https://sonarcloud.io/summary/new_code?id=apache_struts)
|
||||
[](https://deps.dev/maven/org.apache.struts%3Astruts2-core)
|
||||
|
||||
+2
-4
@@ -24,7 +24,8 @@
|
||||
<parent>
|
||||
<groupId>org.apache.struts</groupId>
|
||||
<artifactId>struts2-parent</artifactId>
|
||||
<version>7.0.3</version>
|
||||
<version>7.1.0</version>
|
||||
<relativePath>../parent/pom.xml</relativePath>
|
||||
</parent>
|
||||
<artifactId>struts2-apps</artifactId>
|
||||
<packaging>pom</packaging>
|
||||
@@ -93,13 +94,10 @@
|
||||
<dependency>
|
||||
<groupId>org.apache.struts</groupId>
|
||||
<artifactId>struts2-core</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework</groupId>
|
||||
<artifactId>spring-test</artifactId>
|
||||
<version>${spring.platformVersion}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
@@ -24,12 +24,12 @@
|
||||
<parent>
|
||||
<groupId>org.apache.struts</groupId>
|
||||
<artifactId>struts2-apps</artifactId>
|
||||
<version>7.0.3</version>
|
||||
<version>7.1.0</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>struts2-rest-showcase</artifactId>
|
||||
<packaging>war</packaging>
|
||||
<version>7.0.3</version>
|
||||
<version>7.1.0</version>
|
||||
<name>Struts 2 Rest Showcase Webapp</name>
|
||||
<description>Struts 2 Rest Showcase Example</description>
|
||||
|
||||
@@ -107,7 +107,7 @@
|
||||
<plugin>
|
||||
<groupId>org.eclipse.jetty</groupId>
|
||||
<artifactId>jetty-maven-plugin</artifactId>
|
||||
<version>11.0.18</version>
|
||||
<version>11.0.26</version>
|
||||
<configuration>
|
||||
<stopKey>CTRL+C</stopKey>
|
||||
<stopPort>8999</stopPort>
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
<parent>
|
||||
<groupId>org.apache.struts</groupId>
|
||||
<artifactId>struts2-apps</artifactId>
|
||||
<version>7.0.3</version>
|
||||
<version>7.1.0</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>struts2-showcase</artifactId>
|
||||
@@ -207,7 +207,7 @@
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-failsafe-plugin</artifactId>
|
||||
<version>3.5.2</version>
|
||||
<version>3.5.3</version>
|
||||
<configuration>
|
||||
<includes>
|
||||
<include>it.org.apache.struts2.showcase.*Test</include>
|
||||
@@ -234,7 +234,7 @@
|
||||
<plugin>
|
||||
<groupId>org.eclipse.jetty</groupId>
|
||||
<artifactId>jetty-maven-plugin</artifactId>
|
||||
<version>11.0.18</version>
|
||||
<version>11.0.26</version>
|
||||
<configuration>
|
||||
<stopKey>CTRL+C</stopKey>
|
||||
<stopPort>8999</stopPort>
|
||||
|
||||
@@ -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 it.org.apache.struts2.showcase;
|
||||
|
||||
import org.htmlunit.WebClient;
|
||||
import org.htmlunit.html.HtmlForm;
|
||||
import org.htmlunit.html.HtmlPage;
|
||||
import org.htmlunit.html.HtmlSubmitInput;
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
public class ModelDrivenTest {
|
||||
|
||||
private WebClient webClient;
|
||||
|
||||
@Before
|
||||
public void setUp() throws Exception {
|
||||
webClient = new WebClient();
|
||||
}
|
||||
|
||||
@After
|
||||
public void tearDown() throws Exception {
|
||||
webClient.close();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void submit() throws Exception {
|
||||
HtmlPage page = webClient.getPage(ParameterUtils.getBaseUrl() + "/modelDriven/modelDriven.action");
|
||||
HtmlForm form = page.getForms().get(0);
|
||||
|
||||
form.getInputByName("name").setValue("Johannes");
|
||||
form.getInputByName("age").setValue("21");
|
||||
form.getInputByName("bustedBefore").setChecked(true);
|
||||
form.getTextAreaByName("description").setText("Deals bugs");
|
||||
|
||||
HtmlSubmitInput button = form.getInputByValue("Submit");
|
||||
page = button.click();
|
||||
|
||||
assertThat(page.getElementById("name").asNormalizedText()).isEqualTo("Johannes");
|
||||
assertThat(page.getElementById("age").asNormalizedText()).isEqualTo("21");
|
||||
assertThat(page.getElementById("bustedBefore").asNormalizedText()).isEqualTo("true");
|
||||
assertThat(page.getElementById("description").asNormalizedText()).isEqualTo("Deals bugs");
|
||||
}
|
||||
}
|
||||
+2
-1
@@ -229,7 +229,8 @@ public class StrutsParametersTest {
|
||||
}
|
||||
|
||||
private void assertText(Map<String, String> params, String text) throws IOException {
|
||||
UriComponentsBuilder builder = UriComponentsBuilder.fromHttpUrl(ParameterUtils.getBaseUrl()).path("/paramsannotation/test.action");
|
||||
UriComponentsBuilder builder = UriComponentsBuilder.fromUriString(ParameterUtils.getBaseUrl())
|
||||
.path("/paramsannotation/test.action");
|
||||
params.forEach(builder::queryParam);
|
||||
String url = builder.toUriString();
|
||||
HtmlPage page = webClient.getPage(url);
|
||||
|
||||
+7
-1
@@ -24,7 +24,8 @@
|
||||
<parent>
|
||||
<groupId>org.apache.struts</groupId>
|
||||
<artifactId>struts2-parent</artifactId>
|
||||
<version>7.0.3</version>
|
||||
<version>7.1.0</version>
|
||||
<relativePath>../parent/pom.xml</relativePath>
|
||||
</parent>
|
||||
|
||||
<artifactId>struts2-assembly</artifactId>
|
||||
@@ -171,6 +172,11 @@
|
||||
<artifactId>struts2-jasperreports-plugin</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.apache.struts</groupId>
|
||||
<artifactId>struts2-jasperreports7-plugin</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.apache.struts</groupId>
|
||||
<artifactId>struts2-jfreechart-plugin</artifactId>
|
||||
|
||||
+30
-44
@@ -21,30 +21,18 @@
|
||||
-->
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<parent>
|
||||
<groupId>org.apache.struts</groupId>
|
||||
<artifactId>struts2-parent</artifactId>
|
||||
<version>7.0.3</version>
|
||||
<artifactId>struts2-project</artifactId>
|
||||
<version>7.1.0</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>struts2-bom</artifactId>
|
||||
<version>7.0.3</version>
|
||||
<packaging>pom</packaging>
|
||||
|
||||
<name>Struts 2 Bill of Materials</name>
|
||||
<description>Struts 2 Bill of Materials</description>
|
||||
|
||||
<licenses>
|
||||
<license>
|
||||
<name>The Apache Software License, Version 2.0</name>
|
||||
<url>http://www.apache.org/licenses/LICENSE-2.0.txt</url>
|
||||
<distribution>repo</distribution>
|
||||
</license>
|
||||
</licenses>
|
||||
<name>Struts BOM</name>
|
||||
<description>Struts Bill of Materials (BOM)</description>
|
||||
|
||||
<properties>
|
||||
<struts-version.version>7.0.3</struts-version.version>
|
||||
<maven.site.skip>true</maven.site.skip>
|
||||
<maven.site.deploy.skip>true</maven.site.deploy.skip>
|
||||
</properties>
|
||||
@@ -54,115 +42,113 @@
|
||||
<dependency>
|
||||
<groupId>org.apache.struts</groupId>
|
||||
<artifactId>struts2-core</artifactId>
|
||||
<version>${struts-version.version}</version>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.struts</groupId>
|
||||
<artifactId>struts2-async-plugin</artifactId>
|
||||
<version>${struts-version.version}</version>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.struts</groupId>
|
||||
<artifactId>struts2-bean-validation-plugin</artifactId>
|
||||
<version>${struts-version.version}</version>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.struts</groupId>
|
||||
<artifactId>struts2-cdi-plugin</artifactId>
|
||||
<version>${struts-version.version}</version>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.struts</groupId>
|
||||
<artifactId>struts2-convention-plugin</artifactId>
|
||||
<version>${struts-version.version}</version>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.struts</groupId>
|
||||
<artifactId>struts2-config-browser-plugin</artifactId>
|
||||
<version>${struts-version.version}</version>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.struts</groupId>
|
||||
<artifactId>struts2-embeddedjsp-plugin</artifactId>
|
||||
<version>${struts-version.version}</version>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.struts</groupId>
|
||||
<artifactId>struts2-gxp-plugin</artifactId>
|
||||
<version>${struts-version.version}</version>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.struts</groupId>
|
||||
<artifactId>struts2-jasperreports-plugin</artifactId>
|
||||
<version>${struts-version.version}</version>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.struts</groupId>
|
||||
<artifactId>struts2-jasperreports7-plugin</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.struts</groupId>
|
||||
<artifactId>struts2-javatemplates-plugin</artifactId>
|
||||
<version>${struts-version.version}</version>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.struts</groupId>
|
||||
<artifactId>struts2-jfreechart-plugin</artifactId>
|
||||
<version>${struts-version.version}</version>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.struts</groupId>
|
||||
<artifactId>struts2-json-plugin</artifactId>
|
||||
<version>${struts-version.version}</version>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.struts</groupId>
|
||||
<artifactId>struts2-junit-plugin</artifactId>
|
||||
<version>${struts-version.version}</version>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.struts</groupId>
|
||||
<artifactId>struts2-osgi-plugin</artifactId>
|
||||
<version>${struts-version.version}</version>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.struts</groupId>
|
||||
<artifactId>struts2-plexus-plugin</artifactId>
|
||||
<version>${struts-version.version}</version>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.struts</groupId>
|
||||
<artifactId>struts2-rest-plugin</artifactId>
|
||||
<version>${struts-version.version}</version>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.struts</groupId>
|
||||
<artifactId>struts2-spring-plugin</artifactId>
|
||||
<version>${struts-version.version}</version>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.struts</groupId>
|
||||
<artifactId>struts2-testng-plugin</artifactId>
|
||||
<version>${struts-version.version}</version>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.struts</groupId>
|
||||
<artifactId>struts2-tiles-plugin</artifactId>
|
||||
<version>${struts-version.version}</version>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.struts</groupId>
|
||||
<artifactId>struts2-velocity-plugin</artifactId>
|
||||
<version>${struts-version.version}</version>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.struts</groupId>
|
||||
<artifactId>struts2-xslt-plugin</artifactId>
|
||||
<version>${struts-version.version}</version>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</dependencyManagement>
|
||||
|
||||
<scm>
|
||||
<tag>STRUTS_7_0_3</tag>
|
||||
<connection>scm:git:https://gitbox.apache.org/repos/asf/struts.git</connection>
|
||||
<developerConnection>scm:git:https://gitbox.apache.org/repos/asf/struts.git</developerConnection>
|
||||
<url>https://github.com/apache/struts/</url>
|
||||
</scm>
|
||||
</project>
|
||||
|
||||
+10
-2
@@ -24,7 +24,8 @@
|
||||
<parent>
|
||||
<groupId>org.apache.struts</groupId>
|
||||
<artifactId>struts2-parent</artifactId>
|
||||
<version>7.0.3</version>
|
||||
<version>7.1.0</version>
|
||||
<relativePath>../parent/pom.xml</relativePath>
|
||||
</parent>
|
||||
<artifactId>struts2-core</artifactId>
|
||||
<packaging>jar</packaging>
|
||||
@@ -186,13 +187,19 @@
|
||||
<artifactId>commons-text</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Optional used in org.apache.struts2.util.ProxyUtil to detect if object is HibernateProxy -->
|
||||
<dependency>
|
||||
<!-- Optional used in org.apache.struts2.util.ProxyUtil to detect if object is HibernateProxy -->
|
||||
<groupId>org.hibernate</groupId>
|
||||
<artifactId>hibernate-core</artifactId>
|
||||
<version>5.6.15.Final</version>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<!-- Optional used in org.apache.struts2.util.ProxyUtil to detect if object is Spring proxy -->
|
||||
<groupId>org.springframework</groupId>
|
||||
<artifactId>spring-aop</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework</groupId>
|
||||
@@ -229,6 +236,7 @@
|
||||
<dependency>
|
||||
<groupId>junit</groupId>
|
||||
<artifactId>junit</artifactId>
|
||||
<scope>compile</scope>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
|
||||
|
||||
@@ -18,8 +18,6 @@
|
||||
*/
|
||||
package org.apache.struts2;
|
||||
|
||||
import org.apache.struts2.interceptor.parameter.StrutsParameter;
|
||||
|
||||
/**
|
||||
* ModelDriven Actions provide a model object to be pushed onto the ValueStack
|
||||
* in addition to the Action itself, allowing a FormBean type approach like Struts.
|
||||
@@ -36,7 +34,6 @@ public interface ModelDriven<T> {
|
||||
*
|
||||
* @return the model
|
||||
*/
|
||||
@StrutsParameter(depth = Integer.MAX_VALUE)
|
||||
T getModel();
|
||||
|
||||
}
|
||||
|
||||
@@ -673,4 +673,11 @@ public final class StrutsConstants {
|
||||
* See {@link org.apache.struts2.interceptor.exec.ExecutorProvider}
|
||||
*/
|
||||
public static final String STRUTS_EXECUTOR_PROVIDER = "struts.executor.provider";
|
||||
|
||||
/**
|
||||
* See {@link org.apache.struts2.interceptor.csp.CspNonceReader}
|
||||
* @since 6.8.0
|
||||
*/
|
||||
public static final String STRUTS_CSP_NONCE_READER = "struts.csp.nonce.reader";
|
||||
public static final String STRUTS_CSP_NONCE_SOURCE = "struts.csp.nonce.source";
|
||||
}
|
||||
|
||||
@@ -18,18 +18,17 @@
|
||||
*/
|
||||
package org.apache.struts2.components;
|
||||
|
||||
import org.apache.commons.lang3.ClassUtils;
|
||||
import org.apache.struts2.StrutsException;
|
||||
import org.apache.struts2.dispatcher.PrepareOperations;
|
||||
import org.apache.struts2.inject.Inject;
|
||||
import org.apache.struts2.ognl.ThreadAllowlist;
|
||||
import org.apache.struts2.util.CompoundRoot;
|
||||
import org.apache.struts2.util.ValueStack;
|
||||
import org.apache.struts2.util.reflection.ReflectionProvider;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import org.apache.struts2.StrutsException;
|
||||
import org.apache.struts2.dispatcher.PrepareOperations;
|
||||
import org.apache.struts2.views.annotations.StrutsTag;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import java.io.Writer;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Iterator;
|
||||
@@ -94,9 +93,7 @@ public class Debug extends UIBean {
|
||||
}
|
||||
|
||||
private void allowListClass(Object o) {
|
||||
threadAllowlist.allowClass(o.getClass());
|
||||
ClassUtils.getAllSuperclasses(o.getClass()).forEach(threadAllowlist::allowClass);
|
||||
ClassUtils.getAllInterfaces(o.getClass()).forEach(threadAllowlist::allowClass);
|
||||
threadAllowlist.allowClassHierarchy(o.getClass());
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -18,12 +18,12 @@
|
||||
*/
|
||||
package org.apache.struts2.components;
|
||||
|
||||
import org.apache.struts2.inject.Inject;
|
||||
import org.apache.struts2.util.ValueStack;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.apache.struts2.inject.Inject;
|
||||
import org.apache.struts2.ognl.ThreadAllowlist;
|
||||
import org.apache.struts2.util.MakeIterator;
|
||||
import org.apache.struts2.util.ValueStack;
|
||||
import org.apache.struts2.views.annotations.StrutsTag;
|
||||
import org.apache.struts2.views.annotations.StrutsTagAttribute;
|
||||
import org.apache.struts2.views.jsp.IteratorStatus;
|
||||
@@ -307,7 +307,7 @@ public class IteratorComponent extends ContextBean {
|
||||
stack.push(currentValue);
|
||||
|
||||
if (currentValue != null) {
|
||||
threadAllowlist.allowClass(currentValue.getClass());
|
||||
threadAllowlist.allowClassHierarchy(currentValue.getClass());
|
||||
}
|
||||
|
||||
String var = getVar();
|
||||
|
||||
@@ -36,6 +36,7 @@ import org.apache.struts2.components.template.TemplateEngineManager;
|
||||
import org.apache.struts2.components.template.TemplateRenderingContext;
|
||||
import org.apache.struts2.dispatcher.AttributeMap;
|
||||
import org.apache.struts2.dispatcher.StaticContentLoader;
|
||||
import org.apache.struts2.interceptor.csp.CspNonceReader;
|
||||
import org.apache.struts2.util.ComponentUtils;
|
||||
import org.apache.struts2.util.TextProviderHelper;
|
||||
import org.apache.struts2.views.annotations.StrutsTagAttribute;
|
||||
@@ -528,6 +529,8 @@ public abstract class UIBean extends Component {
|
||||
|
||||
protected TemplateEngineManager templateEngineManager;
|
||||
|
||||
protected CspNonceReader cspNonceReader;
|
||||
|
||||
@Inject(StrutsConstants.STRUTS_UI_TEMPLATEDIR)
|
||||
public void setDefaultTemplateDir(String dir) {
|
||||
this.defaultTemplateDir = dir;
|
||||
@@ -553,6 +556,11 @@ public abstract class UIBean extends Component {
|
||||
this.templateEngineManager = mgr;
|
||||
}
|
||||
|
||||
@Inject
|
||||
public void setCspNonceReader(CspNonceReader cspNonceReader) {
|
||||
this.cspNonceReader = cspNonceReader;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean end(Writer writer, String body) {
|
||||
evaluateParams();
|
||||
@@ -886,13 +894,12 @@ public abstract class UIBean extends Component {
|
||||
}
|
||||
|
||||
// to be used with the CSP interceptor - adds the nonce value as a parameter to be accessed from ftl files
|
||||
HttpSession session = stack.getActionContext().getServletRequest().getSession(false);
|
||||
Object nonceValue = session != null ? session.getAttribute("nonce") : null;
|
||||
CspNonceReader.NonceValue nonceValue = cspNonceReader.readNonceValue(stack);
|
||||
|
||||
if (nonceValue != null) {
|
||||
addParameter("nonce", nonceValue.toString());
|
||||
if (nonceValue.isNonceValueSet()) {
|
||||
addParameter("nonce", nonceValue.getNonceValue());
|
||||
} else {
|
||||
LOG.debug("Session is not active, cannot obtain nonce value");
|
||||
LOG.debug("Nonce not defined in: {}", nonceValue.getSource());
|
||||
}
|
||||
|
||||
evaluateExtraParams();
|
||||
|
||||
@@ -55,6 +55,7 @@ import org.apache.struts2.factory.UnknownHandlerFactory;
|
||||
import org.apache.struts2.factory.ValidatorFactory;
|
||||
import org.apache.struts2.inject.ContainerBuilder;
|
||||
import org.apache.struts2.inject.Scope;
|
||||
import org.apache.struts2.interceptor.csp.CspNonceReader;
|
||||
import org.apache.struts2.interceptor.exec.ExecutorProvider;
|
||||
import org.apache.struts2.ognl.BeanInfoCacheFactory;
|
||||
import org.apache.struts2.ognl.ExpressionCacheFactory;
|
||||
@@ -450,6 +451,8 @@ public class StrutsBeanSelectionProvider extends AbstractBeanSelectionProvider {
|
||||
|
||||
alias(ExecutorProvider.class, StrutsConstants.STRUTS_EXECUTOR_PROVIDER, builder, props, Scope.SINGLETON);
|
||||
|
||||
alias(CspNonceReader.class, StrutsConstants.STRUTS_CSP_NONCE_READER, builder, props, Scope.SINGLETON);
|
||||
|
||||
switchDevMode(props);
|
||||
}
|
||||
|
||||
|
||||
+5
-1
@@ -68,6 +68,8 @@ import org.apache.struts2.validator.ValidatorFileParser;
|
||||
import ognl.PropertyAccessor;
|
||||
import org.apache.struts2.dispatcher.HttpParameters;
|
||||
import org.apache.struts2.dispatcher.Parameter;
|
||||
import org.apache.struts2.interceptor.csp.CspNonceReader;
|
||||
import org.apache.struts2.interceptor.csp.StrutsCspNonceReader;
|
||||
import org.apache.struts2.interceptor.exec.ExecutorProvider;
|
||||
import org.apache.struts2.interceptor.exec.StrutsExecutorProvider;
|
||||
import org.apache.struts2.url.QueryStringBuilder;
|
||||
@@ -159,7 +161,9 @@ public class StrutsDefaultConfigurationProvider implements ConfigurationProvider
|
||||
.factory(UrlEncoder.class, StrutsUrlEncoder.class, Scope.SINGLETON)
|
||||
.factory(UrlDecoder.class, StrutsUrlDecoder.class, Scope.SINGLETON)
|
||||
|
||||
.factory(ExecutorProvider.class, StrutsExecutorProvider.class, Scope.SINGLETON);
|
||||
.factory(ExecutorProvider.class, StrutsExecutorProvider.class, Scope.SINGLETON)
|
||||
|
||||
.factory(CspNonceReader.class, StrutsCspNonceReader.class, Scope.SINGLETON);
|
||||
|
||||
for (Map.Entry<String, Object> entry : DefaultConfiguration.BOOTSTRAP_CONSTANTS.entrySet()) {
|
||||
props.setProperty(entry.getKey(), String.valueOf(entry.getValue()));
|
||||
|
||||
@@ -33,6 +33,7 @@ import java.text.SimpleDateFormat;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.LocalTime;
|
||||
import java.time.OffsetDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.time.format.DateTimeParseException;
|
||||
import java.time.temporal.TemporalAccessor;
|
||||
@@ -116,6 +117,14 @@ public class DateConverter extends DefaultTypeConverter {
|
||||
throw new TypeConversionException("Could not parse date", e);
|
||||
}
|
||||
|
||||
} else if (OffsetDateTime.class == toType) {
|
||||
DateTimeFormatter dtf = DateTimeFormatter.ISO_OFFSET_DATE_TIME;
|
||||
|
||||
try {
|
||||
return OffsetDateTime.parse(sa, dtf);
|
||||
} catch (DateTimeParseException e) {
|
||||
throw new TypeConversionException("Could not parse OffsetDateTime", e);
|
||||
}
|
||||
}
|
||||
|
||||
// final fallback for dates without time
|
||||
|
||||
@@ -25,7 +25,10 @@ import org.apache.struts2.StrutsConstants;
|
||||
import org.apache.struts2.conversion.TypeConversionException;
|
||||
|
||||
import java.lang.reflect.Member;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.LocalTime;
|
||||
import java.time.OffsetDateTime;
|
||||
import java.util.Calendar;
|
||||
import java.util.Collection;
|
||||
import java.util.Date;
|
||||
@@ -100,8 +103,14 @@ public class XWorkBasicConverter extends DefaultTypeConverter {
|
||||
result = doConvertToArray(context, o, member, propertyName, value, toType);
|
||||
} else if (Date.class.isAssignableFrom(toType)) {
|
||||
result = doConvertToDate(context, value, toType);
|
||||
} else if (LocalDate.class.isAssignableFrom(toType)) {
|
||||
result = doConvertToDate(context, value, toType);
|
||||
} else if (LocalDateTime.class.isAssignableFrom(toType)) {
|
||||
result = doConvertToDate(context, value, toType);
|
||||
} else if (LocalTime.class.isAssignableFrom(toType)) {
|
||||
result = doConvertToDate(context, value, toType);
|
||||
} else if (OffsetDateTime.class.isAssignableFrom(toType)) {
|
||||
result = doConvertToDate(context, value, toType);
|
||||
} else if (Calendar.class.isAssignableFrom(toType)) {
|
||||
result = doConvertToCalendar(context, value);
|
||||
} else if (Collection.class.isAssignableFrom(toType)) {
|
||||
|
||||
+84
-12
@@ -18,13 +18,14 @@
|
||||
*/
|
||||
package org.apache.struts2.dispatcher.multipart;
|
||||
|
||||
import org.apache.struts2.inject.Inject;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import org.apache.commons.fileupload2.core.DiskFileItemFactory;
|
||||
import org.apache.commons.fileupload2.core.FileUploadByteCountLimitException;
|
||||
import org.apache.commons.fileupload2.core.FileUploadContentTypeException;
|
||||
import org.apache.commons.fileupload2.core.FileUploadException;
|
||||
import org.apache.commons.fileupload2.core.FileUploadFileCountLimitException;
|
||||
import org.apache.commons.fileupload2.core.FileUploadSizeException;
|
||||
import org.apache.commons.fileupload2.core.RequestContext;
|
||||
import org.apache.commons.fileupload2.jakarta.servlet6.JakartaServletDiskFileUpload;
|
||||
import org.apache.commons.io.FilenameUtils;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
@@ -32,7 +33,9 @@ import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.apache.struts2.StrutsConstants;
|
||||
import org.apache.struts2.dispatcher.LocalizedMessage;
|
||||
import org.apache.struts2.inject.Inject;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.Charset;
|
||||
import java.nio.file.Path;
|
||||
@@ -42,6 +45,9 @@ import java.util.Enumeration;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
import static org.apache.commons.lang3.StringUtils.normalizeSpace;
|
||||
|
||||
/**
|
||||
* Abstract class with some helper methods, it should be used
|
||||
@@ -187,7 +193,21 @@ public abstract class AbstractMultiPartRequest implements MultiPartRequest {
|
||||
* @param charset used charset from incoming request
|
||||
* @param saveDir a temporary folder to store uploaded files (not always needed)
|
||||
*/
|
||||
protected abstract JakartaServletDiskFileUpload createJakartaFileUpload(Charset charset, Path saveDir);
|
||||
protected JakartaServletDiskFileUpload createJakartaFileUpload(Charset charset, Path saveDir) {
|
||||
DiskFileItemFactory.Builder builder = DiskFileItemFactory.builder();
|
||||
|
||||
LOG.debug("Using file save directory: {}", saveDir);
|
||||
builder.setPath(saveDir);
|
||||
|
||||
LOG.debug("Sets buffer size: {}", bufferSize);
|
||||
builder.setBufferSize(bufferSize);
|
||||
|
||||
LOG.debug("Using charset: {}", charset);
|
||||
builder.setCharset(charset);
|
||||
|
||||
DiskFileItemFactory factory = builder.get();
|
||||
return new JakartaServletDiskFileUpload(factory);
|
||||
}
|
||||
|
||||
protected JakartaServletDiskFileUpload prepareServletFileUpload(Charset charset, Path saveDir) {
|
||||
JakartaServletDiskFileUpload servletFileUpload = createJakartaFileUpload(charset, saveDir);
|
||||
@@ -207,11 +227,15 @@ public abstract class AbstractMultiPartRequest implements MultiPartRequest {
|
||||
return servletFileUpload;
|
||||
}
|
||||
|
||||
protected RequestContext createRequestContext(HttpServletRequest request) {
|
||||
return new StrutsRequestContext(request);
|
||||
}
|
||||
|
||||
protected boolean exceedsMaxStringLength(String fieldName, String fieldValue) {
|
||||
if (maxStringLength != null && fieldValue.length() > maxStringLength) {
|
||||
if (LOG.isDebugEnabled()) {
|
||||
LOG.debug("Form field: {} of size: {} bytes exceeds limit of: {}.",
|
||||
sanitizeNewlines(fieldName), fieldValue.length(), maxStringLength);
|
||||
normalizeSpace(fieldName), fieldValue.length(), maxStringLength);
|
||||
}
|
||||
LocalizedMessage localizedMessage = new LocalizedMessage(this.getClass(),
|
||||
STRUTS_MESSAGES_UPLOAD_ERROR_PARAMETER_TOO_LONG_KEY, null,
|
||||
@@ -234,7 +258,7 @@ public abstract class AbstractMultiPartRequest implements MultiPartRequest {
|
||||
try {
|
||||
processUpload(request, saveDir);
|
||||
} catch (FileUploadException e) {
|
||||
LOG.debug("Error parsing the multi-part request!", e);
|
||||
LOG.warn("Error parsing the multi-part request!", e);
|
||||
Class<? extends Throwable> exClass = FileUploadException.class;
|
||||
Object[] args = new Object[]{};
|
||||
|
||||
@@ -257,7 +281,7 @@ public abstract class AbstractMultiPartRequest implements MultiPartRequest {
|
||||
errors.add(errorMessage);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
LOG.debug("Unable to parse request", e);
|
||||
LOG.warn("Unable to parse request", e);
|
||||
LocalizedMessage errorMessage = buildErrorMessage(e.getClass(), e.getMessage(), new Object[]{});
|
||||
if (!errors.contains(errorMessage)) {
|
||||
errors.add(errorMessage);
|
||||
@@ -288,13 +312,6 @@ public abstract class AbstractMultiPartRequest implements MultiPartRequest {
|
||||
return FilenameUtils.getName(originalFileName);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated since 7.0.1, use {@link StringUtils#normalizeSpace(String)} instead
|
||||
*/
|
||||
@Deprecated
|
||||
protected String sanitizeNewlines(String before) {
|
||||
return before.replaceAll("\\R", "_");
|
||||
}
|
||||
|
||||
/* (non-Javadoc)
|
||||
* @see org.apache.struts2.dispatcher.multipart.MultiPartRequest#getErrors()
|
||||
@@ -384,6 +401,61 @@ public abstract class AbstractMultiPartRequest implements MultiPartRequest {
|
||||
return values.toArray(new String[0]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a secure temporary file in the specified directory using UUID-based naming.
|
||||
* This method ensures files are created in a controlled location rather than the
|
||||
* system temporary directory, reducing security risks.
|
||||
*
|
||||
* @param fileName the original filename for logging purposes
|
||||
* @param location the directory where the temporary file should be created
|
||||
* @return a new temporary file in the specified location
|
||||
*/
|
||||
protected File createTemporaryFile(String fileName, Path location) {
|
||||
String uid = UUID.randomUUID().toString().replace("-", "_");
|
||||
File file = location.resolve("upload_" + uid + ".tmp").toFile();
|
||||
LOG.debug("Creating temporary file: {} (originally: {})", file.getName(), fileName);
|
||||
return file;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates that an uploaded file is not empty (0 bytes) and adds an error if it is.
|
||||
*
|
||||
* <p>Empty file uploads are rejected as they are not considered valid uploads.
|
||||
* This validation ensures consistent behavior across all multipart implementations
|
||||
* and provides proper user feedback when empty files are uploaded.</p>
|
||||
*
|
||||
* <p>When an empty file is detected:</p>
|
||||
* <ul>
|
||||
* <li>A debug log message is written with field name and filename</li>
|
||||
* <li>A localized error message is created and added to the errors list</li>
|
||||
* <li>The method returns true to indicate the file should be rejected</li>
|
||||
* </ul>
|
||||
*
|
||||
* @param fileSize the size of the uploaded file in bytes
|
||||
* @param fileName the original filename of the uploaded file
|
||||
* @param fieldName the form field name containing the file upload
|
||||
* @return true if the file is empty and should be rejected, false otherwise
|
||||
* @see #buildErrorMessage(Class, String, Object[])
|
||||
*/
|
||||
protected boolean rejectEmptyFile(long fileSize, String fileName, String fieldName) {
|
||||
if (fileSize == 0) {
|
||||
if (LOG.isDebugEnabled()) {
|
||||
LOG.debug("Rejecting empty file upload for field: {} with filename: {}",
|
||||
normalizeSpace(fieldName), normalizeSpace(fileName));
|
||||
}
|
||||
LocalizedMessage errorMessage = buildErrorMessage(
|
||||
IllegalArgumentException.class,
|
||||
"Empty files are not allowed",
|
||||
new Object[]{fileName, fieldName}
|
||||
);
|
||||
if (!errors.contains(errorMessage)) {
|
||||
errors.add(errorMessage);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/* (non-Javadoc)
|
||||
* @see org.apache.struts2.dispatcher.multipart.MultiPartRequest#cleanUp()
|
||||
*/
|
||||
|
||||
+278
-33
@@ -20,14 +20,17 @@ package org.apache.struts2.dispatcher.multipart;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import org.apache.commons.fileupload2.core.DiskFileItem;
|
||||
import org.apache.commons.fileupload2.core.DiskFileItemFactory;
|
||||
import org.apache.commons.fileupload2.core.RequestContext;
|
||||
import org.apache.commons.fileupload2.jakarta.servlet6.JakartaServletDiskFileUpload;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.apache.struts2.dispatcher.LocalizedMessage;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.Charset;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
@@ -36,11 +39,73 @@ import static org.apache.commons.lang3.StringUtils.normalizeSpace;
|
||||
|
||||
/**
|
||||
* Multipart form data request adapter for Jakarta Commons FileUpload package.
|
||||
*
|
||||
* <p>This implementation provides secure handling of multipart requests with proper
|
||||
* resource management and cleanup. It tracks all temporary files created during
|
||||
* the upload process and ensures they are properly cleaned up to prevent
|
||||
* resource leaks and security vulnerabilities.</p>
|
||||
*
|
||||
* <p>Key features:</p>
|
||||
* <ul>
|
||||
* <li>Automatic tracking and cleanup of temporary files</li>
|
||||
* <li>Proper error handling with user-friendly error messages</li>
|
||||
* <li>Support for both in-memory and disk-based file uploads</li>
|
||||
* <li>Extensible cleanup mechanisms for customization</li>
|
||||
* </ul>
|
||||
*
|
||||
* <p>Usage example:</p>
|
||||
* <pre>
|
||||
* JakartaMultiPartRequest multipartRequest = new JakartaMultiPartRequest();
|
||||
* try {
|
||||
* multipartRequest.parse(request, "/tmp/uploads");
|
||||
* // Process uploaded files
|
||||
* for (String fieldName : multipartRequest.getFileParameterNames()) {
|
||||
* List<UploadedFile> files = multipartRequest.getFile(fieldName);
|
||||
* // Handle files
|
||||
* }
|
||||
* } finally {
|
||||
* multipartRequest.cleanUp(); // Always clean up resources
|
||||
* }
|
||||
* </pre>
|
||||
*
|
||||
* @see AbstractMultiPartRequest
|
||||
* @see org.apache.commons.fileupload2.jakarta.servlet6.JakartaServletDiskFileUpload
|
||||
*/
|
||||
public class JakartaMultiPartRequest extends AbstractMultiPartRequest {
|
||||
|
||||
private static final Logger LOG = LogManager.getLogger(JakartaMultiPartRequest.class);
|
||||
|
||||
/**
|
||||
* List to track all DiskFileItem instances for proper cleanup
|
||||
*/
|
||||
private final List<DiskFileItem> diskFileItems = new ArrayList<>();
|
||||
|
||||
/**
|
||||
* List to track temporary files created for in-memory uploads
|
||||
*/
|
||||
private final List<File> temporaryFiles = new ArrayList<>();
|
||||
|
||||
/**
|
||||
* Processes the multipart upload request using Jakarta Commons FileUpload.
|
||||
*
|
||||
* <p>This method handles the core upload processing by:</p>
|
||||
* <ol>
|
||||
* <li>Reading the character encoding from the request</li>
|
||||
* <li>Preparing the Jakarta servlet file upload handler</li>
|
||||
* <li>Creating a request context for processing</li>
|
||||
* <li>Iterating through all form items (fields and files)</li>
|
||||
* <li>Processing each item appropriately based on its type</li>
|
||||
* </ol>
|
||||
*
|
||||
* <p>All {@link org.apache.commons.fileupload2.core.DiskFileItem} instances
|
||||
* are automatically tracked for proper cleanup.</p>
|
||||
*
|
||||
* @param request the HTTP servlet request containing the multipart data
|
||||
* @param saveDir the directory where uploaded files will be stored
|
||||
* @throws IOException if an error occurs during upload processing
|
||||
* @see #processNormalFormField(DiskFileItem, Charset)
|
||||
* @see #processFileField(DiskFileItem, String)
|
||||
*/
|
||||
@Override
|
||||
protected void processUpload(HttpServletRequest request, String saveDir) throws IOException {
|
||||
Charset charset = readCharsetEncoding(request);
|
||||
@@ -48,44 +113,54 @@ public class JakartaMultiPartRequest extends AbstractMultiPartRequest {
|
||||
JakartaServletDiskFileUpload servletFileUpload =
|
||||
prepareServletFileUpload(charset, Path.of(saveDir));
|
||||
|
||||
for (DiskFileItem item : servletFileUpload.parseRequest(request)) {
|
||||
RequestContext requestContext = createRequestContext(request);
|
||||
|
||||
for (DiskFileItem item : servletFileUpload.parseRequest(requestContext)) {
|
||||
// Track all DiskFileItem instances for cleanup - this is critical for security
|
||||
// as it ensures temporary files are properly cleaned up even if processing fails
|
||||
diskFileItems.add(item);
|
||||
|
||||
LOG.debug(() -> "Processing a form field: " + normalizeSpace(item.getFieldName()));
|
||||
if (item.isFormField()) {
|
||||
// Process regular form fields (text inputs, checkboxes, etc.)
|
||||
processNormalFormField(item, charset);
|
||||
} else {
|
||||
// Process file upload fields
|
||||
LOG.debug(() -> "Processing a file: " + normalizeSpace(item.getFieldName()));
|
||||
processFileField(item);
|
||||
processFileField(item, saveDir);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected JakartaServletDiskFileUpload createJakartaFileUpload(Charset charset, Path saveDir) {
|
||||
DiskFileItemFactory.Builder builder = DiskFileItemFactory.builder();
|
||||
|
||||
LOG.debug("Using file save directory: {}", saveDir);
|
||||
builder.setPath(saveDir);
|
||||
|
||||
LOG.debug("Sets minimal buffer size to always write file to disk");
|
||||
builder.setBufferSize(1);
|
||||
|
||||
LOG.debug("Using charset: {}", charset);
|
||||
builder.setCharset(charset);
|
||||
|
||||
DiskFileItemFactory factory = builder.get();
|
||||
return new JakartaServletDiskFileUpload(factory);
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes a normal form field (non-file) from the multipart request.
|
||||
*
|
||||
* <p>This method handles text form fields by:</p>
|
||||
* <ol>
|
||||
* <li>Validating the field name is not null</li>
|
||||
* <li>Extracting the field value using the specified charset</li>
|
||||
* <li>Checking if the field value exceeds maximum string length</li>
|
||||
* <li>Adding the value to the parameters map</li>
|
||||
* </ol>
|
||||
*
|
||||
* <p>Fields with null names are skipped with a warning log message.</p>
|
||||
* <p>Empty form fields are stored as empty strings.</p>
|
||||
*
|
||||
* @param item the disk file item representing the form field
|
||||
* @param charset the character set to use for decoding the field value
|
||||
* @throws IOException if an error occurs reading the field value
|
||||
* @see #exceedsMaxStringLength(String, String)
|
||||
*/
|
||||
protected void processNormalFormField(DiskFileItem item, Charset charset) throws IOException {
|
||||
LOG.debug("Item: {} is a normal form field", normalizeSpace(item.getName()));
|
||||
|
||||
List<String> values;
|
||||
String fieldName = item.getFieldName();
|
||||
if (parameters.get(fieldName) != null) {
|
||||
values = parameters.get(fieldName);
|
||||
} else {
|
||||
values = new ArrayList<>();
|
||||
if (fieldName == null) {
|
||||
LOG.warn("Form field has null fieldName, skipping");
|
||||
return;
|
||||
}
|
||||
|
||||
List<String> values = parameters.computeIfAbsent(fieldName, k -> new ArrayList<>());
|
||||
|
||||
String fieldValue = item.getString(charset);
|
||||
if (exceedsMaxStringLength(fieldName, fieldValue)) {
|
||||
@@ -99,22 +174,81 @@ public class JakartaMultiPartRequest extends AbstractMultiPartRequest {
|
||||
parameters.put(fieldName, values);
|
||||
}
|
||||
|
||||
protected void processFileField(DiskFileItem item) {
|
||||
/**
|
||||
* Processes a file field from the multipart request.
|
||||
*
|
||||
* <p>This method handles file uploads by:</p>
|
||||
* <ol>
|
||||
* <li>Validating the file name and field name are not null/empty</li>
|
||||
* <li>Determining if the file is stored in memory or on disk</li>
|
||||
* <li>For in-memory files: creating a temporary file and copying content</li>
|
||||
* <li>For disk files: using the existing file directly</li>
|
||||
* <li>Creating an {@link UploadedFile} abstraction</li>
|
||||
* <li>Adding the file to the uploaded files collection</li>
|
||||
* </ol>
|
||||
*
|
||||
* <p>Temporary files created for in-memory uploads are automatically
|
||||
* tracked for cleanup. Any errors during temporary file creation are
|
||||
* logged and added to the error list for user feedback.</p>
|
||||
*
|
||||
* @param item the disk file item representing the uploaded file
|
||||
* @see #cleanUpTemporaryFiles()
|
||||
*/
|
||||
protected void processFileField(DiskFileItem item, String saveDir) {
|
||||
// Skip file uploads that don't have a file name - meaning that no file was selected.
|
||||
if (item.getName() == null || item.getName().trim().isEmpty()) {
|
||||
LOG.debug(() -> "No file has been uploaded for the field: " + normalizeSpace(item.getFieldName()));
|
||||
return;
|
||||
}
|
||||
|
||||
List<UploadedFile> values;
|
||||
if (uploadedFiles.get(item.getFieldName()) != null) {
|
||||
values = uploadedFiles.get(item.getFieldName());
|
||||
} else {
|
||||
values = new ArrayList<>();
|
||||
String fieldName = item.getFieldName();
|
||||
if (fieldName == null) {
|
||||
LOG.warn("File field has null fieldName, skipping");
|
||||
return;
|
||||
}
|
||||
|
||||
// Reject empty files (0 bytes) as they are not considered valid uploads
|
||||
if (rejectEmptyFile(item.getSize(), item.getName(), fieldName)) {
|
||||
return;
|
||||
}
|
||||
|
||||
List<UploadedFile> values = uploadedFiles.computeIfAbsent(fieldName, k -> new ArrayList<>());
|
||||
|
||||
if (item.isInMemory()) {
|
||||
LOG.warn(() -> "Storing uploaded files just in memory isn't supported currently, skipping file: %s!".formatted(normalizeSpace(item.getName())));
|
||||
LOG.debug(() -> "Creating temporary file representing in-memory uploaded item: " + normalizeSpace(item.getFieldName()));
|
||||
try {
|
||||
File tempFile = createTemporaryFile(item.getName(), Path.of(saveDir));
|
||||
|
||||
// Track the temporary file for explicit cleanup
|
||||
temporaryFiles.add(tempFile);
|
||||
|
||||
// Write the in-memory content to the temporary file
|
||||
try (java.io.FileOutputStream fos = new java.io.FileOutputStream(tempFile)) {
|
||||
fos.write(item.get());
|
||||
}
|
||||
|
||||
UploadedFile uploadedFile = StrutsUploadedFile.Builder
|
||||
.create(tempFile)
|
||||
.withOriginalName(item.getName())
|
||||
.withContentType(item.getContentType())
|
||||
.withInputName(item.getFieldName())
|
||||
.build();
|
||||
values.add(uploadedFile);
|
||||
|
||||
if (LOG.isDebugEnabled()) {
|
||||
LOG.debug("Created temporary file for in-memory uploaded item: {} at {}",
|
||||
normalizeSpace(item.getName()), tempFile.getAbsolutePath());
|
||||
}
|
||||
} catch (IOException e) {
|
||||
LOG.warn("Failed to create temporary file for in-memory uploaded item: {}",
|
||||
normalizeSpace(item.getName()), e);
|
||||
|
||||
// Add the error to the errors list for proper user feedback
|
||||
LocalizedMessage errorMessage = buildErrorMessage(e.getClass(), e.getMessage(), new Object[]{item.getName()});
|
||||
if (!errors.contains(errorMessage)) {
|
||||
errors.add(errorMessage);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
UploadedFile uploadedFile = StrutsUploadedFile.Builder
|
||||
.create(item.getPath().toFile())
|
||||
@@ -125,7 +259,118 @@ public class JakartaMultiPartRequest extends AbstractMultiPartRequest {
|
||||
values.add(uploadedFile);
|
||||
}
|
||||
|
||||
uploadedFiles.put(item.getFieldName(), values);
|
||||
uploadedFiles.put(fieldName, values);
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleans up disk file items by deleting associated temporary files.
|
||||
*
|
||||
* <p>This method iterates through all tracked {@link DiskFileItem} instances
|
||||
* and performs cleanup operations:</p>
|
||||
* <ul>
|
||||
* <li>For in-memory items: logs cleanup (no files to delete)</li>
|
||||
* <li>For disk items: deletes the associated temporary file</li>
|
||||
* </ul>
|
||||
*
|
||||
* <p>This method is called automatically during {@link #cleanUp()} but can
|
||||
* be overridden by subclasses to customize cleanup behavior. All exceptions
|
||||
* are caught and logged to prevent cleanup failures from affecting the
|
||||
* overall cleanup process.</p>
|
||||
*
|
||||
* @see #cleanUp()
|
||||
* @see #cleanUpTemporaryFiles()
|
||||
*/
|
||||
protected void cleanUpDiskFileItems() {
|
||||
LOG.debug("Clean up all DiskFileItem instances (both form fields and file uploads");
|
||||
for (DiskFileItem item : diskFileItems) {
|
||||
try {
|
||||
if (item.isInMemory()) {
|
||||
LOG.debug(() -> "Cleaning up in-memory item: " + normalizeSpace(item.getFieldName()));
|
||||
} else {
|
||||
Path itemPath = item.getPath();
|
||||
if (LOG.isDebugEnabled()) {
|
||||
LOG.debug("Cleaning up disk item: {} at {}", normalizeSpace(item.getFieldName()), itemPath);
|
||||
}
|
||||
if (!Files.deleteIfExists(itemPath)) {
|
||||
LOG.warn("There was a problem attempting to delete uploaded file: {}", itemPath);
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
LOG.warn("Error cleaning up DiskFileItem: {}", normalizeSpace(item.getFieldName()), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleans up temporary files created for in-memory uploads.
|
||||
*
|
||||
* <p>This method deletes all temporary files that were created when
|
||||
* processing in-memory uploads. These files are created in
|
||||
* {@link #processFileField(DiskFileItem, String)} when an uploaded file is
|
||||
* stored in memory and needs to be written to disk.</p>
|
||||
*
|
||||
* <p>The cleanup process:</p>
|
||||
* <ol>
|
||||
* <li>Iterates through all tracked temporary files</li>
|
||||
* <li>Checks if each file still exists</li>
|
||||
* <li>Attempts to delete existing files</li>
|
||||
* <li>Logs warnings for files that cannot be deleted</li>
|
||||
* </ol>
|
||||
*
|
||||
* <p>This method can be overridden by subclasses to customize cleanup
|
||||
* behavior. All exceptions are caught and logged to ensure cleanup
|
||||
* continues even if individual file deletions fail.</p>
|
||||
*
|
||||
* @see #cleanUp()
|
||||
* @see #cleanUpDiskFileItems()
|
||||
*/
|
||||
protected void cleanUpTemporaryFiles() {
|
||||
LOG.debug("Cleaning up {} temporary files created for in-memory uploads", temporaryFiles.size());
|
||||
for (File tempFile : temporaryFiles) {
|
||||
try {
|
||||
if (!Files.deleteIfExists(tempFile.toPath())) {
|
||||
LOG.warn("There was a problem attempting to delete temporary file: {}", tempFile.getAbsolutePath());
|
||||
}
|
||||
} catch (Exception e) {
|
||||
LOG.warn("Error cleaning up temporary file: {}", tempFile.getAbsolutePath(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs complete cleanup of all resources associated with this request.
|
||||
*
|
||||
* <p>This method extends the parent cleanup functionality to ensure proper
|
||||
* cleanup of Jakarta-specific resources:</p>
|
||||
* <ol>
|
||||
* <li>Calls parent cleanup to handle base class resources</li>
|
||||
* <li>Cleans up all tracked {@link DiskFileItem} instances</li>
|
||||
* <li>Cleans up all temporary files created for in-memory uploads</li>
|
||||
* <li>Clears internal tracking collections</li>
|
||||
* </ol>
|
||||
*
|
||||
* <p>This method is designed to be safe to call multiple times and will
|
||||
* not throw exceptions even if cleanup operations fail. All errors are
|
||||
* logged for debugging purposes.</p>
|
||||
*
|
||||
* <p><strong>Important:</strong> This method should always be called in a
|
||||
* finally block to ensure resources are properly released, even if
|
||||
* exceptions occur during request processing.</p>
|
||||
*
|
||||
* @see #cleanUpDiskFileItems()
|
||||
* @see #cleanUpTemporaryFiles()
|
||||
* @see AbstractMultiPartRequest#cleanUp()
|
||||
*/
|
||||
@Override
|
||||
public void cleanUp() {
|
||||
super.cleanUp();
|
||||
try {
|
||||
cleanUpDiskFileItems();
|
||||
cleanUpTemporaryFiles();
|
||||
} finally {
|
||||
diskFileItems.clear();
|
||||
temporaryFiles.clear();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+105
-66
@@ -19,7 +19,6 @@
|
||||
package org.apache.struts2.dispatcher.multipart;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import org.apache.commons.fileupload2.core.DiskFileItemFactory;
|
||||
import org.apache.commons.fileupload2.core.FileItemInput;
|
||||
import org.apache.commons.fileupload2.core.FileUploadFileCountLimitException;
|
||||
import org.apache.commons.fileupload2.core.FileUploadSizeException;
|
||||
@@ -40,12 +39,11 @@ import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
import static org.apache.commons.lang3.StringUtils.normalizeSpace;
|
||||
|
||||
/**
|
||||
* Multi-part form data request adapter for Jakarta Commons FileUpload package that
|
||||
* Multipart form data request adapter for Jakarta Commons FileUpload package that
|
||||
* leverages the streaming API rather than the traditional non-streaming API.
|
||||
* <p>
|
||||
* For more details see WW-3025
|
||||
@@ -82,51 +80,62 @@ public class JakartaStreamMultiPartRequest extends AbstractMultiPartRequest {
|
||||
});
|
||||
}
|
||||
|
||||
protected JakartaServletDiskFileUpload createJakartaFileUpload(Charset charset, Path location) {
|
||||
DiskFileItemFactory.Builder builder = DiskFileItemFactory.builder();
|
||||
|
||||
LOG.debug("Using file save directory: {}", location);
|
||||
builder.setPath(location);
|
||||
|
||||
LOG.debug("Sets buffer size: {}", bufferSize);
|
||||
builder.setBufferSize(bufferSize);
|
||||
|
||||
LOG.debug("Using charset: {}", charset);
|
||||
builder.setCharset(charset);
|
||||
|
||||
DiskFileItemFactory factory = builder.get();
|
||||
return new JakartaServletDiskFileUpload(factory);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads the entire contents of an input stream into a string.
|
||||
*
|
||||
* <p>This method uses a buffered approach to efficiently read the stream
|
||||
* content without loading the entire stream into memory at once. It uses
|
||||
* try-with-resources to ensure proper cleanup of resources.</p>
|
||||
*
|
||||
* @param inputStream the input stream to read from
|
||||
* @return the stream contents as a UTF-8 string
|
||||
* @throws IOException if an error occurs reading the stream
|
||||
*/
|
||||
private String readStream(InputStream inputStream) throws IOException {
|
||||
ByteArrayOutputStream result = new ByteArrayOutputStream();
|
||||
byte[] buffer = new byte[1024];
|
||||
for (int length; (length = inputStream.read(buffer)) != -1; ) {
|
||||
result.write(buffer, 0, length);
|
||||
// Use try-with-resources to ensure ByteArrayOutputStream is properly closed
|
||||
try (ByteArrayOutputStream result = new ByteArrayOutputStream()) {
|
||||
byte[] buffer = new byte[1024]; // 1KB buffer for efficient reading
|
||||
// Read the stream in chunks to avoid loading everything into memory at once
|
||||
for (int length; (length = inputStream.read(buffer)) != -1; ) {
|
||||
result.write(buffer, 0, length);
|
||||
}
|
||||
// Convert to string using UTF-8 encoding
|
||||
return result.toString(StandardCharsets.UTF_8);
|
||||
}
|
||||
return result.toString(StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes the FileItem as a normal form field.
|
||||
*
|
||||
* @param fileItemInput a form field item input
|
||||
* Processes a normal form field (non-file) from the multipart request using streaming API.
|
||||
*
|
||||
* <p>This method handles text form fields by:</p>
|
||||
* <ol>
|
||||
* <li>Validating the field name is not null</li>
|
||||
* <li>Reading the field value from the input stream</li>
|
||||
* <li>Checking if the field value exceeds maximum string length</li>
|
||||
* <li>Adding the value to the parameters collection</li>
|
||||
* </ol>
|
||||
*
|
||||
* <p>Fields with null names are skipped with a warning log message.</p>
|
||||
* <p>The streaming approach is more memory-efficient for large form data.</p>
|
||||
*
|
||||
* @param fileItemInput a form field item input from the streaming API
|
||||
* @throws IOException if an error occurs reading the input stream
|
||||
* @see #readStream(InputStream)
|
||||
* @see #exceedsMaxStringLength(String, String)
|
||||
*/
|
||||
protected void processFileItemAsFormField(FileItemInput fileItemInput) throws IOException {
|
||||
String fieldName = fileItemInput.getFieldName();
|
||||
if (fieldName == null) {
|
||||
LOG.warn("Form field has null fieldName, skipping");
|
||||
return;
|
||||
}
|
||||
|
||||
String fieldValue = readStream(fileItemInput.getInputStream());
|
||||
|
||||
if (exceedsMaxStringLength(fieldName, fieldValue)) {
|
||||
return;
|
||||
}
|
||||
|
||||
List<String> values;
|
||||
if (parameters.containsKey(fieldName)) {
|
||||
values = parameters.get(fieldName);
|
||||
} else {
|
||||
values = new ArrayList<>();
|
||||
parameters.put(fieldName, values);
|
||||
}
|
||||
List<String> values = parameters.computeIfAbsent(fieldName, k -> new ArrayList<>());
|
||||
values.add(fieldValue);
|
||||
}
|
||||
|
||||
@@ -181,10 +190,28 @@ public class JakartaStreamMultiPartRequest extends AbstractMultiPartRequest {
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes the FileItem as a file field.
|
||||
*
|
||||
* @param fileItemInput file item representing upload file
|
||||
* @param location location
|
||||
* Processes a file field from the multipart request using streaming API.
|
||||
*
|
||||
* <p>This method handles file uploads by:</p>
|
||||
* <ol>
|
||||
* <li>Validating the file name and field name are not null/empty</li>
|
||||
* <li>Checking if the upload exceeds maximum file count</li>
|
||||
* <li>Creating a temporary file in the specified location</li>
|
||||
* <li>Streaming the file content directly to disk</li>
|
||||
* <li>Checking if the total size exceeds maximum allowed size</li>
|
||||
* <li>Creating an {@link UploadedFile} abstraction or cleaning up on size exceeded</li>
|
||||
* </ol>
|
||||
*
|
||||
* <p>Files with null names or field names are skipped with appropriate logging.</p>
|
||||
* <p>The streaming approach is more memory-efficient for large file uploads
|
||||
* as it writes directly to disk rather than loading into memory first.</p>
|
||||
*
|
||||
* @param fileItemInput file item representing upload file from streaming API
|
||||
* @param location the directory where temporary files will be created
|
||||
* @throws IOException if an error occurs during file processing
|
||||
* @see #createTemporaryFile(String, Path)
|
||||
* @see #streamFileToDisk(FileItemInput, File)
|
||||
* @see #createUploadedFile(FileItemInput, File)
|
||||
*/
|
||||
protected void processFileItemAsFileField(FileItemInput fileItemInput, Path location) throws IOException {
|
||||
// Skip file uploads that don't have a file name - meaning that no file was selected.
|
||||
@@ -192,6 +219,12 @@ public class JakartaStreamMultiPartRequest extends AbstractMultiPartRequest {
|
||||
LOG.debug(() -> "No file has been uploaded for the field: " + normalizeSpace(fileItemInput.getFieldName()));
|
||||
return;
|
||||
}
|
||||
|
||||
// Skip file uploads that don't have a field name
|
||||
if (fileItemInput.getFieldName() == null) {
|
||||
LOG.warn("File upload has null fieldName, skipping");
|
||||
return;
|
||||
}
|
||||
|
||||
if (exceedsMaxFiles(fileItemInput)) {
|
||||
return;
|
||||
@@ -199,6 +232,15 @@ public class JakartaStreamMultiPartRequest extends AbstractMultiPartRequest {
|
||||
|
||||
File file = createTemporaryFile(fileItemInput.getName(), location);
|
||||
streamFileToDisk(fileItemInput, file);
|
||||
|
||||
// Reject empty files (0 bytes) as they are not considered valid uploads
|
||||
if (rejectEmptyFile(file.length(), fileItemInput.getName(), fileItemInput.getFieldName())) {
|
||||
// Clean up the empty temporary file
|
||||
if (!Files.deleteIfExists(file.toPath())) {
|
||||
LOG.warn("Failed to delete empty temporary file: {}", file.getAbsolutePath());
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
Long currentFilesSize = maxSizeOfFiles != null ? actualSizeOfUploadedFiles() : null;
|
||||
if (maxSizeOfFiles != null && currentFilesSize + file.length() >= maxSizeOfFiles) {
|
||||
@@ -208,20 +250,6 @@ public class JakartaStreamMultiPartRequest extends AbstractMultiPartRequest {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a temporary file based on the given filename and location.
|
||||
*
|
||||
* @param fileName file name
|
||||
* @param location location
|
||||
* @return a temporary file based on the given filename and location
|
||||
*/
|
||||
protected File createTemporaryFile(String fileName, Path location) {
|
||||
String uid = UUID.randomUUID().toString().replace("-", "_");
|
||||
File file = location.resolve("upload_" + uid + ".tmp").toFile();
|
||||
LOG.debug("Creating temporary file: {} (originally: {})", file.getName(), fileName);
|
||||
return file;
|
||||
}
|
||||
|
||||
/**
|
||||
* Streams the file upload stream to the specified file.
|
||||
*
|
||||
@@ -240,29 +268,40 @@ public class JakartaStreamMultiPartRequest extends AbstractMultiPartRequest {
|
||||
}
|
||||
|
||||
/**
|
||||
* Create {@link UploadedFile} abstraction over uploaded file
|
||||
*
|
||||
* @param fileItemInput file item stream
|
||||
* @param file the file
|
||||
* Creates an {@link UploadedFile} abstraction over an uploaded file.
|
||||
*
|
||||
* <p>This method creates a wrapper around the uploaded file that provides
|
||||
* a consistent interface for accessing file information and content.
|
||||
* The created {@link UploadedFile} instance contains:</p>
|
||||
* <ul>
|
||||
* <li>The original filename as provided by the client</li>
|
||||
* <li>The content type (MIME type) if available</li>
|
||||
* <li>The form field name that contained the file</li>
|
||||
* <li>A reference to the temporary file on disk</li>
|
||||
* </ul>
|
||||
*
|
||||
* <p>The file is automatically added to the uploaded files collection,
|
||||
* grouped by field name to support multiple file uploads per field.</p>
|
||||
*
|
||||
* @param fileItemInput file item stream containing file metadata
|
||||
* @param file the temporary file containing the uploaded content
|
||||
* @see UploadedFile
|
||||
* @see StrutsUploadedFile
|
||||
*/
|
||||
protected void createUploadedFile(FileItemInput fileItemInput, File file) {
|
||||
String fileName = fileItemInput.getName();
|
||||
String fieldName = fileItemInput.getFieldName();
|
||||
|
||||
|
||||
// fieldName null check already done in processFileItemAsFileField
|
||||
UploadedFile uploadedFile = StrutsUploadedFile.Builder
|
||||
.create(file)
|
||||
.withOriginalName(fileName)
|
||||
.withContentType(fileItemInput.getContentType())
|
||||
.withInputName(fileItemInput.getFieldName())
|
||||
.withInputName(fieldName)
|
||||
.build();
|
||||
|
||||
if (uploadedFiles.containsKey(fieldName)) {
|
||||
uploadedFiles.get(fieldName).add(uploadedFile);
|
||||
} else {
|
||||
List<UploadedFile> infos = new ArrayList<>();
|
||||
infos.add(uploadedFile);
|
||||
uploadedFiles.put(fieldName, infos);
|
||||
}
|
||||
List<UploadedFile> infos = uploadedFiles.computeIfAbsent(fieldName, key -> new ArrayList<>());
|
||||
infos.add(uploadedFile);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.struts2.dispatcher.multipart;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import org.apache.commons.fileupload2.jakarta.servlet6.JakartaServletRequestContext;
|
||||
|
||||
/**
|
||||
* Provides a specialized request context for Struts applications,
|
||||
* extending the Jakarta Servlet request context to add custom handling
|
||||
* for multipart-related requests.
|
||||
* <p>
|
||||
* This class overrides multipart detection logic to ensure that requests
|
||||
* without a content type are not treated as multipart, improving robustness
|
||||
* in file upload scenarios.
|
||||
*/
|
||||
public class StrutsRequestContext extends JakartaServletRequestContext {
|
||||
|
||||
/**
|
||||
* Constructs a context for this request.
|
||||
*
|
||||
* @param request The request to which this context applies.
|
||||
*/
|
||||
public StrutsRequestContext(HttpServletRequest request) {
|
||||
super(request);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if the current request is multipart-related.
|
||||
* <p>
|
||||
* This implementation first checks if the request's content type is set.
|
||||
* If the content type is {@code null}, it returns {@code false} immediately.
|
||||
* Otherwise, it delegates to the superclass implementation to perform
|
||||
* further checks.
|
||||
*
|
||||
* @return {@code true} if the request is multipart-related; {@code false} otherwise.
|
||||
*/
|
||||
@Override
|
||||
public boolean isMultipartRelated() {
|
||||
if (this.getRequest().getContentType() == null) {
|
||||
return false;
|
||||
}
|
||||
return super.isMultipartRelated();
|
||||
}
|
||||
}
|
||||
@@ -18,11 +18,11 @@
|
||||
*/
|
||||
package org.apache.struts2.factory;
|
||||
|
||||
import org.apache.struts2.conversion.TypeConverter;
|
||||
import org.apache.struts2.inject.Container;
|
||||
import org.apache.struts2.inject.Inject;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.apache.struts2.ObjectFactory;
|
||||
import org.apache.struts2.conversion.TypeConverter;
|
||||
import org.apache.struts2.inject.Inject;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
@@ -30,19 +30,18 @@ import java.util.Map;
|
||||
* Default implementation
|
||||
*/
|
||||
public class StrutsConverterFactory implements ConverterFactory {
|
||||
|
||||
private static final Logger LOG = LogManager.getLogger(StrutsConverterFactory.class);
|
||||
|
||||
private Container container;
|
||||
private ObjectFactory objectFactory;
|
||||
|
||||
@Inject
|
||||
public void setContainer(Container container) {
|
||||
this.container = container;
|
||||
public void setObjectFactory(ObjectFactory objectFactory) {
|
||||
this.objectFactory = objectFactory;
|
||||
}
|
||||
|
||||
@Override
|
||||
public TypeConverter buildConverter(Class<? extends TypeConverter> converterClass, Map<String, Object> extraContext) throws Exception {
|
||||
LOG.debug("Creating converter of type [{}]", converterClass.getCanonicalName());
|
||||
return container.inject(converterClass);
|
||||
return (TypeConverter)objectFactory.buildBean(converterClass, extraContext);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+1
-1
@@ -115,7 +115,7 @@ public abstract class AbstractFileUploadInterceptor extends AbstractInterceptor
|
||||
}
|
||||
|
||||
// If it's null the upload failed
|
||||
if (file == null) {
|
||||
if (file == null || file.getContent() == null) {
|
||||
String errMsg = getTextMessage(action, STRUTS_MESSAGES_ERROR_UPLOADING_KEY, new String[]{inputName});
|
||||
if (validation != null) {
|
||||
validation.addFieldError(inputName, errMsg);
|
||||
|
||||
@@ -20,11 +20,11 @@ package org.apache.struts2.interceptor;
|
||||
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.apache.struts2.result.ActionChainResult;
|
||||
import org.apache.struts2.ActionInvocation;
|
||||
import org.apache.struts2.StrutsConstants;
|
||||
import org.apache.struts2.Unchainable;
|
||||
import org.apache.struts2.inject.Inject;
|
||||
import org.apache.struts2.result.ActionChainResult;
|
||||
import org.apache.struts2.result.Result;
|
||||
import org.apache.struts2.util.CompoundRoot;
|
||||
import org.apache.struts2.util.ProxyUtil;
|
||||
@@ -167,17 +167,18 @@ public class ChainingInterceptor extends AbstractInterceptor {
|
||||
}
|
||||
|
||||
private void copyStack(ActionInvocation invocation, CompoundRoot root) {
|
||||
List list = prepareList(root);
|
||||
List<Object> list = prepareList(root);
|
||||
Map<String, Object> ctxMap = invocation.getInvocationContext().getContextMap();
|
||||
for (Object object : list) {
|
||||
if (shouldCopy(object)) {
|
||||
Object action = invocation.getAction();
|
||||
Class<?> editable = null;
|
||||
if(ProxyUtil.isProxy(action)) {
|
||||
editable = ProxyUtil.ultimateTargetClass(action);
|
||||
}
|
||||
reflectionProvider.copy(object, action, ctxMap, prepareExcludes(), includes, editable);
|
||||
if (!shouldCopy(object)) {
|
||||
continue;
|
||||
}
|
||||
Object action = invocation.getAction();
|
||||
Class<?> editable = null;
|
||||
if (ProxyUtil.isProxy(action)) {
|
||||
editable = ProxyUtil.ultimateTargetClass(action);
|
||||
}
|
||||
reflectionProvider.copy(object, action, ctxMap, prepareExcludes(), includes, editable);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -204,9 +205,8 @@ public class ChainingInterceptor extends AbstractInterceptor {
|
||||
return o != null && !(o instanceof Unchainable);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private List prepareList(CompoundRoot root) {
|
||||
List list = new ArrayList(root);
|
||||
private List<Object> prepareList(CompoundRoot root) {
|
||||
var list = new ArrayList<>(root);
|
||||
list.remove(0);
|
||||
Collections.reverse(list);
|
||||
return list;
|
||||
|
||||
@@ -215,9 +215,9 @@ public class ExceptionMappingInterceptor extends AbstractInterceptor {
|
||||
HttpParameters parameters = HttpParameters.create(mappingParams).build();
|
||||
invocation.getInvocationContext().withParameters(parameters);
|
||||
result = mappingConfig.getResult();
|
||||
ExceptionHolder holder = new ExceptionHolder(e);
|
||||
threadAllowlist.allowClass(holder.getClass());
|
||||
threadAllowlist.allowClass(e.getClass());
|
||||
var holder = new ExceptionHolder(e);
|
||||
threadAllowlist.allowClassHierarchy(ExceptionHolder.class);
|
||||
threadAllowlist.allowClassHierarchy(e.getClass());
|
||||
publishException(invocation, holder);
|
||||
} else {
|
||||
throw e;
|
||||
|
||||
@@ -20,6 +20,8 @@ package org.apache.struts2.interceptor;
|
||||
|
||||
import org.apache.struts2.ActionInvocation;
|
||||
import org.apache.struts2.ModelDriven;
|
||||
import org.apache.struts2.inject.Inject;
|
||||
import org.apache.struts2.ognl.ThreadAllowlist;
|
||||
import org.apache.struts2.util.CompoundRoot;
|
||||
import org.apache.struts2.util.ValueStack;
|
||||
|
||||
@@ -79,20 +81,27 @@ import org.apache.struts2.util.ValueStack;
|
||||
public class ModelDrivenInterceptor extends AbstractInterceptor {
|
||||
|
||||
protected boolean refreshModelBeforeResult = false;
|
||||
private ThreadAllowlist threadAllowlist;
|
||||
|
||||
public void setRefreshModelBeforeResult(boolean val) {
|
||||
this.refreshModelBeforeResult = val;
|
||||
}
|
||||
|
||||
@Inject
|
||||
public void setThreadAllowlist(ThreadAllowlist threadAllowlist) {
|
||||
this.threadAllowlist = threadAllowlist;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String intercept(ActionInvocation invocation) throws Exception {
|
||||
Object action = invocation.getAction();
|
||||
|
||||
if (action instanceof ModelDriven modelDriven) {
|
||||
if (action instanceof ModelDriven<?> modelDriven) {
|
||||
ValueStack stack = invocation.getStack();
|
||||
Object model = modelDriven.getModel();
|
||||
if (model != null) {
|
||||
stack.push(model);
|
||||
if (model != null) {
|
||||
stack.push(model);
|
||||
threadAllowlist.allowClassHierarchy(model.getClass());
|
||||
}
|
||||
if (refreshModelBeforeResult) {
|
||||
invocation.addPreResultListener(new RefreshModelBeforeResult(modelDriven, model));
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
/*
|
||||
* 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.csp;
|
||||
|
||||
import org.apache.struts2.util.ValueStack;
|
||||
|
||||
/**
|
||||
* Reads the nonce value using the ValueStack, {@link StrutsCspNonceReader} is the default implementation
|
||||
* @since 6.8.0
|
||||
*/
|
||||
public interface CspNonceReader {
|
||||
|
||||
NonceValue readNonceValue(ValueStack stack);
|
||||
|
||||
class NonceValue {
|
||||
private final String nonceValue;
|
||||
private final CspNonceSource source;
|
||||
|
||||
private NonceValue(String nonceValue, CspNonceSource source) {
|
||||
this.nonceValue = nonceValue;
|
||||
this.source = source;
|
||||
}
|
||||
|
||||
public static NonceValue ofSession(String nonceValue) {
|
||||
return new NonceValue(nonceValue, CspNonceSource.SESSION);
|
||||
}
|
||||
|
||||
public static NonceValue ofRequest(String nonceValue) {
|
||||
return new NonceValue(nonceValue, CspNonceSource.REQUEST);
|
||||
}
|
||||
|
||||
public static NonceValue ofNullSession() {
|
||||
return new NonceValue(null, CspNonceSource.SESSION);
|
||||
}
|
||||
|
||||
public static NonceValue ofNullRequest() {
|
||||
return new NonceValue(null, CspNonceSource.REQUEST);
|
||||
}
|
||||
|
||||
public boolean isNonceValueSet() {
|
||||
return nonceValue != null;
|
||||
}
|
||||
|
||||
public String getNonceValue() {
|
||||
return nonceValue;
|
||||
}
|
||||
|
||||
public CspNonceSource getSource() {
|
||||
return source;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
String displayNonce;
|
||||
if (nonceValue != null && nonceValue.length() >= 4) {
|
||||
displayNonce = String.format("nonceValue='%s**********'", nonceValue.substring(0, 4));
|
||||
} else if (nonceValue != null) {
|
||||
displayNonce = String.format("nonceValue='%s**********'", nonceValue);
|
||||
} else {
|
||||
displayNonce = "nonceValue='<null>'";
|
||||
}
|
||||
return "NonceValue{" +
|
||||
displayNonce +
|
||||
", source=" + source +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
/*
|
||||
* 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.csp;
|
||||
|
||||
/**
|
||||
* Source of the nonce value
|
||||
*/
|
||||
public enum CspNonceSource {
|
||||
REQUEST,
|
||||
SESSION
|
||||
}
|
||||
@@ -44,6 +44,11 @@ public interface CspSettings {
|
||||
String HTTPS = "https:";
|
||||
String CSP_REPORT_TYPE = "application/csp-report";
|
||||
|
||||
/**
|
||||
* Adds CSP related headers to response based on request state (e.g., if session has been created)
|
||||
*
|
||||
* @since Struts 6.0.3
|
||||
*/
|
||||
void addCspHeaders(HttpServletRequest request, HttpServletResponse response);
|
||||
|
||||
/**
|
||||
|
||||
@@ -20,13 +20,15 @@ package org.apache.struts2.interceptor.csp;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import org.apache.struts2.inject.Inject;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.apache.struts2.StrutsConstants;
|
||||
import org.apache.struts2.action.CspSettingsAware;
|
||||
|
||||
import java.security.SecureRandom;
|
||||
import java.util.Base64;
|
||||
import java.util.Objects;
|
||||
|
||||
import static java.lang.String.format;
|
||||
|
||||
@@ -43,63 +45,86 @@ import static java.lang.String.format;
|
||||
*/
|
||||
public class DefaultCspSettings implements CspSettings {
|
||||
|
||||
private final static Logger LOG = LogManager.getLogger(DefaultCspSettings.class);
|
||||
private static final Logger LOG = LogManager.getLogger(DefaultCspSettings.class);
|
||||
private static final String NONCE_KEY = "nonce";
|
||||
|
||||
private final SecureRandom sRand = new SecureRandom();
|
||||
|
||||
private CspNonceSource nonceSource = CspNonceSource.SESSION;
|
||||
|
||||
protected String reportUri;
|
||||
protected String reportTo;
|
||||
// default to reporting mode
|
||||
protected String cspHeader = CSP_REPORT_HEADER;
|
||||
|
||||
@Inject(value = StrutsConstants.STRUTS_CSP_NONCE_SOURCE, required = false)
|
||||
public void setNonceSource(String nonceSource) {
|
||||
if (StringUtils.isBlank(nonceSource)) {
|
||||
this.nonceSource = CspNonceSource.SESSION;
|
||||
} else {
|
||||
this.nonceSource = CspNonceSource.valueOf(nonceSource.toUpperCase());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addCspHeaders(HttpServletRequest request, HttpServletResponse response) {
|
||||
if (this.nonceSource == CspNonceSource.SESSION) {
|
||||
addCspHeadersWithSession(request, response);
|
||||
} else if (this.nonceSource == CspNonceSource.REQUEST) {
|
||||
addCspHeadersWithRequest(request, response);
|
||||
} else {
|
||||
LOG.warn("Unknown nonce source: {}, ignoring CSP settings", nonceSource);
|
||||
}
|
||||
}
|
||||
|
||||
private void addCspHeadersWithSession(HttpServletRequest request, HttpServletResponse response) {
|
||||
if (isSessionActive(request)) {
|
||||
LOG.trace("Session is active, applying CSP settings");
|
||||
associateNonceWithSession(request);
|
||||
response.setHeader(cspHeader, createPolicyFormat(request));
|
||||
String nonceValue = generateNonceValue();
|
||||
request.getSession().setAttribute(NONCE_KEY, nonceValue);
|
||||
response.setHeader(cspHeader, createPolicyFormat(nonceValue));
|
||||
} else {
|
||||
LOG.trace("Session is not active, ignoring CSP settings");
|
||||
LOG.debug("Session is not active, ignoring CSP settings");
|
||||
}
|
||||
}
|
||||
|
||||
private void addCspHeadersWithRequest(HttpServletRequest request, HttpServletResponse response) {
|
||||
String nonceValue = generateNonceValue();
|
||||
request.setAttribute(NONCE_KEY, nonceValue);
|
||||
response.setHeader(cspHeader, createPolicyFormat(nonceValue));
|
||||
}
|
||||
|
||||
private boolean isSessionActive(HttpServletRequest request) {
|
||||
return request.getSession(false) != null;
|
||||
}
|
||||
|
||||
private void associateNonceWithSession(HttpServletRequest request) {
|
||||
String nonceValue = Base64.getUrlEncoder().encodeToString(getRandomBytes());
|
||||
request.getSession().setAttribute("nonce", nonceValue);
|
||||
private String generateNonceValue() {
|
||||
return Base64.getUrlEncoder().encodeToString(getRandomBytes());
|
||||
}
|
||||
|
||||
protected String createPolicyFormat(HttpServletRequest request) {
|
||||
StringBuilder policyFormatBuilder = new StringBuilder()
|
||||
.append(OBJECT_SRC)
|
||||
.append(format(" '%s'; ", NONE))
|
||||
.append(SCRIPT_SRC)
|
||||
.append(" 'nonce-%s' ") // nonce placeholder
|
||||
.append(format("'%s' ", STRICT_DYNAMIC))
|
||||
.append(format("%s %s; ", HTTP, HTTPS))
|
||||
.append(BASE_URI)
|
||||
.append(format(" '%s'; ", NONE));
|
||||
protected String createPolicyFormat(String nonceValue) {
|
||||
StringBuilder builder = new StringBuilder()
|
||||
.append(OBJECT_SRC)
|
||||
.append(format(" '%s'; ", NONE))
|
||||
.append(SCRIPT_SRC)
|
||||
.append(format(" 'nonce-%s' ", nonceValue))
|
||||
.append(format("'%s' ", STRICT_DYNAMIC))
|
||||
.append(format("%s %s; ", HTTP, HTTPS))
|
||||
.append(BASE_URI)
|
||||
.append(format(" '%s'; ", NONE));
|
||||
|
||||
if (reportUri != null) {
|
||||
policyFormatBuilder
|
||||
.append(REPORT_URI)
|
||||
.append(format(" %s; ", reportUri));
|
||||
if(reportTo != null) {
|
||||
policyFormatBuilder
|
||||
builder
|
||||
.append(REPORT_URI)
|
||||
.append(format(" %s; ", reportUri));
|
||||
if (reportTo != null) {
|
||||
builder
|
||||
.append(REPORT_TO)
|
||||
.append(format(" %s; ", reportTo));
|
||||
}
|
||||
}
|
||||
|
||||
return format(policyFormatBuilder.toString(), getNonceString(request));
|
||||
}
|
||||
|
||||
protected String getNonceString(HttpServletRequest request) {
|
||||
Object nonce = request.getSession().getAttribute("nonce");
|
||||
return Objects.toString(nonce);
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
private byte[] getRandomBytes() {
|
||||
@@ -128,10 +153,10 @@ public class DefaultCspSettings implements CspSettings {
|
||||
@Override
|
||||
public String toString() {
|
||||
return "DefaultCspSettings{" +
|
||||
"reportUri='" + reportUri + '\'' +
|
||||
", reportTo='" + reportTo + '\'' +
|
||||
", cspHeader='" + cspHeader + '\'' +
|
||||
'}';
|
||||
"reportUri='" + reportUri + '\'' +
|
||||
", reportTo='" + reportTo + '\'' +
|
||||
", cspHeader='" + cspHeader + '\'' +
|
||||
'}';
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
/*
|
||||
* 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.csp;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpSession;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.apache.struts2.StrutsConstants;
|
||||
import org.apache.struts2.inject.Inject;
|
||||
import org.apache.struts2.util.ValueStack;
|
||||
|
||||
/**
|
||||
* Reads nonce value from session or request attribute.
|
||||
* @since 6.8.0
|
||||
*/
|
||||
public class StrutsCspNonceReader implements CspNonceReader {
|
||||
|
||||
private static final Logger LOG = LogManager.getLogger(StrutsCspNonceReader.class);
|
||||
|
||||
private final CspNonceSource nonceSource;
|
||||
|
||||
@Inject(value = StrutsConstants.STRUTS_CSP_NONCE_SOURCE, required = false)
|
||||
public StrutsCspNonceReader(String source) {
|
||||
if (StringUtils.isBlank(source)) {
|
||||
this.nonceSource = CspNonceSource.SESSION;
|
||||
} else {
|
||||
this.nonceSource = CspNonceSource.valueOf(source.toUpperCase());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public NonceValue readNonceValue(ValueStack stack) {
|
||||
HttpServletRequest request = stack.getActionContext().getServletRequest();
|
||||
NonceValue nonceValue;
|
||||
|
||||
if (nonceSource == CspNonceSource.SESSION) {
|
||||
LOG.debug("Reading nonce value from session");
|
||||
nonceValue = readNonceFromSession(request);
|
||||
} else if (nonceSource == CspNonceSource.REQUEST) {
|
||||
LOG.debug("Reading nonce value from request attribute");
|
||||
nonceValue = readNonceFromRequest(request);
|
||||
} else {
|
||||
LOG.warn("Unknown nonce source: {}, reading nonce value from session", nonceSource);
|
||||
nonceValue = readNonceFromSession(request);
|
||||
}
|
||||
return nonceValue;
|
||||
}
|
||||
|
||||
private NonceValue readNonceFromSession(HttpServletRequest request) {
|
||||
HttpSession session = request.getSession(false);
|
||||
Object nonceValue = session != null ? session.getAttribute("nonce") : null;
|
||||
if (nonceValue == null) {
|
||||
LOG.debug("Session is not active, cannot obtain nonce value");
|
||||
return NonceValue.ofNullSession();
|
||||
}
|
||||
return NonceValue.ofSession(nonceValue.toString());
|
||||
}
|
||||
|
||||
private NonceValue readNonceFromRequest(HttpServletRequest request) {
|
||||
Object nonceValue = request.getAttribute("nonce");
|
||||
if (nonceValue == null) {
|
||||
LOG.warn("Request attribute 'nonce' is not set, cannot obtain nonce value");
|
||||
return NonceValue.ofNullRequest();
|
||||
}
|
||||
|
||||
return NonceValue.ofRequest(nonceValue.toString());
|
||||
}
|
||||
}
|
||||
+2
-5
@@ -18,8 +18,6 @@
|
||||
*/
|
||||
package org.apache.struts2.interceptor.debugging;
|
||||
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import org.apache.commons.lang3.ClassUtils;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.apache.struts2.ActionContext;
|
||||
@@ -38,6 +36,7 @@ import org.apache.struts2.util.reflection.ReflectionProvider;
|
||||
import org.apache.struts2.views.freemarker.FreemarkerManager;
|
||||
import org.apache.struts2.views.freemarker.FreemarkerResult;
|
||||
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import java.beans.BeanInfo;
|
||||
import java.beans.Introspector;
|
||||
import java.beans.PropertyDescriptor;
|
||||
@@ -273,9 +272,7 @@ public class DebuggingInterceptor extends AbstractInterceptor {
|
||||
|
||||
private void allowListClass(Object o) {
|
||||
if (o != null) {
|
||||
threadAllowlist.allowClass(o.getClass());
|
||||
ClassUtils.getAllSuperclasses(o.getClass()).forEach(threadAllowlist::allowClass);
|
||||
ClassUtils.getAllInterfaces(o.getClass()).forEach(threadAllowlist::allowClass);
|
||||
threadAllowlist.allowClassHierarchy(o.getClass());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+32
-19
@@ -19,7 +19,6 @@
|
||||
package org.apache.struts2.interceptor.parameter;
|
||||
|
||||
import org.apache.commons.lang3.BooleanUtils;
|
||||
import org.apache.commons.lang3.ClassUtils;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.apache.struts2.ActionContext;
|
||||
@@ -33,12 +32,14 @@ import org.apache.struts2.dispatcher.HttpParameters;
|
||||
import org.apache.struts2.dispatcher.Parameter;
|
||||
import org.apache.struts2.inject.Inject;
|
||||
import org.apache.struts2.interceptor.MethodFilterInterceptor;
|
||||
import org.apache.struts2.ognl.OgnlUtil;
|
||||
import org.apache.struts2.ognl.ThreadAllowlist;
|
||||
import org.apache.struts2.security.AcceptedPatternsChecker;
|
||||
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.TextParseUtil;
|
||||
import org.apache.struts2.util.ValueStack;
|
||||
import org.apache.struts2.util.ValueStackFactory;
|
||||
@@ -46,7 +47,6 @@ import org.apache.struts2.util.reflection.ReflectionContextState;
|
||||
|
||||
import java.beans.BeanInfo;
|
||||
import java.beans.IntrospectionException;
|
||||
import java.beans.Introspector;
|
||||
import java.beans.PropertyDescriptor;
|
||||
import java.lang.reflect.AnnotatedElement;
|
||||
import java.lang.reflect.Field;
|
||||
@@ -93,6 +93,7 @@ public class ParametersInterceptor extends MethodFilterInterceptor {
|
||||
protected boolean requireAnnotationsTransitionMode = false;
|
||||
|
||||
private ValueStackFactory valueStackFactory;
|
||||
private OgnlUtil ognlUtil;
|
||||
protected ThreadAllowlist threadAllowlist;
|
||||
private ExcludedPatternsChecker excludedPatterns;
|
||||
private AcceptedPatternsChecker acceptedPatterns;
|
||||
@@ -104,6 +105,11 @@ public class ParametersInterceptor extends MethodFilterInterceptor {
|
||||
this.valueStackFactory = valueStackFactory;
|
||||
}
|
||||
|
||||
@Inject
|
||||
public void setOgnlUtil(OgnlUtil ognlUtil) {
|
||||
this.ognlUtil = ognlUtil;
|
||||
}
|
||||
|
||||
@Inject
|
||||
public void setThreadAllowlist(ThreadAllowlist threadAllowlist) {
|
||||
this.threadAllowlist = threadAllowlist;
|
||||
@@ -351,9 +357,8 @@ public class ParametersInterceptor extends MethodFilterInterceptor {
|
||||
long paramDepth = name.codePoints().mapToObj(c -> (char) c).filter(NESTING_CHARS::contains).count();
|
||||
|
||||
if (action instanceof ModelDriven<?> && !ActionContext.getContext().getValueStack().peek().equals(action)) {
|
||||
LOG.debug("Model driven Action detected, exempting from @StrutsParameter annotation requirement and OGNL allowlisting model type");
|
||||
// (Exempted by annotation on org.apache.struts2.ModelDriven#getModel)
|
||||
return hasValidAnnotatedMember("model", action, paramDepth + 1);
|
||||
LOG.debug("Model driven Action detected, exempting from @StrutsParameter annotation requirement");
|
||||
return true;
|
||||
}
|
||||
|
||||
if (requireAnnotationsTransitionMode && paramDepth == 0) {
|
||||
@@ -395,6 +400,7 @@ public class ParametersInterceptor extends MethodFilterInterceptor {
|
||||
}
|
||||
|
||||
protected boolean hasValidAnnotatedPropertyDescriptor(Object action, PropertyDescriptor propDesc, long paramDepth) {
|
||||
Class<?> actionClass = ultimateClass(action);
|
||||
Method relevantMethod = paramDepth == 0 ? propDesc.getWriteMethod() : propDesc.getReadMethod();
|
||||
if (relevantMethod == null) {
|
||||
return false;
|
||||
@@ -412,7 +418,7 @@ public class ParametersInterceptor extends MethodFilterInterceptor {
|
||||
return false;
|
||||
}
|
||||
LOG.debug("Success: Matching annotated method [{}] found for property [{}] of depth [{}] on Action [{}]",
|
||||
relevantMethod.getName(), propDesc.getName(), paramDepth, action.getClass().getSimpleName());
|
||||
relevantMethod.getName(), propDesc.getName(), paramDepth, actionClass.getSimpleName());
|
||||
if (paramDepth >= 1) {
|
||||
allowlistClass(propDesc.getPropertyType());
|
||||
}
|
||||
@@ -439,36 +445,35 @@ public class ParametersInterceptor extends MethodFilterInterceptor {
|
||||
}
|
||||
|
||||
protected void allowlistParamType(Type paramType) {
|
||||
if (paramType instanceof Class) {
|
||||
allowlistClass((Class<?>) paramType);
|
||||
if (paramType instanceof Class<?> clazz) {
|
||||
allowlistClass(clazz);
|
||||
}
|
||||
}
|
||||
|
||||
protected void allowlistClass(Class<?> clazz) {
|
||||
threadAllowlist.allowClass(clazz);
|
||||
ClassUtils.getAllSuperclasses(clazz).forEach(threadAllowlist::allowClass);
|
||||
ClassUtils.getAllInterfaces(clazz).forEach(threadAllowlist::allowClass);
|
||||
threadAllowlist.allowClassHierarchy(clazz);
|
||||
}
|
||||
|
||||
protected boolean hasValidAnnotatedField(Object action, String fieldName, long paramDepth) {
|
||||
Class<?> actionClass = ultimateClass(action);
|
||||
LOG.debug("No matching annotated method found for property [{}] of depth [{}] on Action [{}], now also checking for public field",
|
||||
fieldName, paramDepth, action.getClass().getSimpleName());
|
||||
fieldName, paramDepth, actionClass.getSimpleName());
|
||||
Field field;
|
||||
try {
|
||||
field = action.getClass().getDeclaredField(fieldName);
|
||||
field = actionClass.getDeclaredField(fieldName);
|
||||
} catch (NoSuchFieldException e) {
|
||||
LOG.debug("Matching field for property [{}] not found on Action [{}]", fieldName, action.getClass().getSimpleName());
|
||||
LOG.debug("Matching field for property [{}] not found on Action [{}]", fieldName, actionClass.getSimpleName());
|
||||
return false;
|
||||
}
|
||||
if (!Modifier.isPublic(field.getModifiers())) {
|
||||
LOG.debug("Matching field [{}] is not public on Action [{}]", field.getName(), action.getClass().getSimpleName());
|
||||
LOG.debug("Matching field [{}] is not public on Action [{}]", field.getName(), actionClass.getSimpleName());
|
||||
return false;
|
||||
}
|
||||
if (getPermittedInjectionDepth(field) < paramDepth) {
|
||||
String logMessage = format(
|
||||
"Parameter injection for field [%s] on Action [%s] rejected. Ensure it is annotated with @StrutsParameter with an appropriate 'depth'.",
|
||||
field.getName(),
|
||||
action.getClass().getName());
|
||||
actionClass.getName());
|
||||
if (devMode) {
|
||||
notifyDeveloperOfError(LOG, action, logMessage);
|
||||
} else {
|
||||
@@ -477,7 +482,7 @@ public class ParametersInterceptor extends MethodFilterInterceptor {
|
||||
return false;
|
||||
}
|
||||
LOG.debug("Success: Matching annotated public field [{}] found for property of depth [{}] on Action [{}]",
|
||||
field.getName(), paramDepth, action.getClass().getSimpleName());
|
||||
field.getName(), paramDepth, actionClass.getSimpleName());
|
||||
if (paramDepth >= 1) {
|
||||
allowlistClass(field.getType());
|
||||
}
|
||||
@@ -510,11 +515,19 @@ public class ParametersInterceptor extends MethodFilterInterceptor {
|
||||
return element.getAnnotation(StrutsParameter.class);
|
||||
}
|
||||
|
||||
protected Class<?> ultimateClass(Object action) {
|
||||
if (ProxyUtil.isProxy(action)) {
|
||||
return ProxyUtil.ultimateTargetClass(action);
|
||||
}
|
||||
return action.getClass();
|
||||
}
|
||||
|
||||
protected BeanInfo getBeanInfo(Object action) {
|
||||
Class<?> actionClass = ultimateClass(action);
|
||||
try {
|
||||
return Introspector.getBeanInfo(action.getClass());
|
||||
return ognlUtil.getBeanInfo(actionClass);
|
||||
} catch (IntrospectionException e) {
|
||||
LOG.warn("Error introspecting Action {} for parameter injection validation", action.getClass(), e);
|
||||
LOG.warn("Error introspecting Action {} for parameter injection validation", actionClass, e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,21 +15,37 @@
|
||||
*/
|
||||
package org.apache.struts2.ognl;
|
||||
|
||||
import java.util.function.Function;
|
||||
|
||||
import static java.util.Objects.requireNonNull;
|
||||
|
||||
/**
|
||||
* A basic cache interface for use with OGNL processing (such as Expression, BeanInfo).
|
||||
* All OGNL caches will have an eviction limit, but setting an extremely high value can
|
||||
* simulate an "effectively unlimited" cache.
|
||||
* A basic cache interface for use with OGNL processing (such as Expression, BeanInfo). Implementation must be
|
||||
* thread-safe. All OGNL caches will have an eviction limit, but setting an extremely high value can simulate an
|
||||
* "effectively unlimited" cache.
|
||||
*
|
||||
* @param <Key> The type for the cache key entries
|
||||
* @param <Value> The type for the cache value entries
|
||||
* @param <K> The type for the cache key entries
|
||||
* @param <V> The type for the cache value entries
|
||||
*/
|
||||
public interface OgnlCache<Key, Value> {
|
||||
public interface OgnlCache<K, V> {
|
||||
|
||||
Value get(Key key);
|
||||
V get(K key);
|
||||
|
||||
void put(Key key, Value value);
|
||||
/**
|
||||
* @since 7.1
|
||||
*/
|
||||
default V computeIfAbsent(K key,
|
||||
Function<? super K, ? extends V> mappingFunction) {
|
||||
requireNonNull(mappingFunction);
|
||||
if (get(key) == null) {
|
||||
putIfAbsent(key, mappingFunction.apply(key));
|
||||
}
|
||||
return get(key);
|
||||
}
|
||||
|
||||
void putIfAbsent(Key key, Value value);
|
||||
void put(K key, V value);
|
||||
|
||||
void putIfAbsent(K key, V value);
|
||||
|
||||
int size();
|
||||
|
||||
|
||||
@@ -18,6 +18,8 @@ package org.apache.struts2.ognl;
|
||||
import com.github.benmanes.caffeine.cache.Cache;
|
||||
import com.github.benmanes.caffeine.cache.Caffeine;
|
||||
|
||||
import java.util.function.Function;
|
||||
|
||||
/**
|
||||
* <p>This OGNL Cache implementation is backed by {@link Caffeine} which uses the Window TinyLfu algorithm.</p>
|
||||
*
|
||||
@@ -46,6 +48,11 @@ public class OgnlCaffeineCache<K, V> implements OgnlCache<K, V> {
|
||||
return cache.getIfPresent(key);
|
||||
}
|
||||
|
||||
@Override
|
||||
public V computeIfAbsent(K key, Function<? super K, ? extends V> mappingFunction) {
|
||||
return cache.asMap().computeIfAbsent(key, mappingFunction);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void put(K key, V value) {
|
||||
cache.put(key, value);
|
||||
|
||||
@@ -17,6 +17,7 @@ package org.apache.struts2.ognl;
|
||||
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.function.Function;
|
||||
|
||||
/**
|
||||
* <p>Basic OGNL cache implementation.</p>
|
||||
@@ -45,16 +46,21 @@ public class OgnlDefaultCache<K, V> implements OgnlCache<K, V> {
|
||||
return ognlCache.get(key);
|
||||
}
|
||||
|
||||
@Override
|
||||
public V computeIfAbsent(K key, Function<? super K, ? extends V> mappingFunction) {
|
||||
return ognlCache.computeIfAbsent(key, mappingFunction);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void put(K key, V value) {
|
||||
ognlCache.put(key, value);
|
||||
this.clearIfEvictionLimitExceeded();
|
||||
clearIfEvictionLimitExceeded();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void putIfAbsent(K key, V value) {
|
||||
ognlCache.putIfAbsent(key, value);
|
||||
this.clearIfEvictionLimitExceeded();
|
||||
clearIfEvictionLimitExceeded();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -69,12 +75,12 @@ public class OgnlDefaultCache<K, V> implements OgnlCache<K, V> {
|
||||
|
||||
@Override
|
||||
public int getEvictionLimit() {
|
||||
return this.cacheEvictionLimit.get();
|
||||
return cacheEvictionLimit.get();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setEvictionLimit(int cacheEvictionLimit) {
|
||||
this.cacheEvictionLimit.set(cacheEvictionLimit);
|
||||
public void setEvictionLimit(int newCacheEvictionLimit) {
|
||||
cacheEvictionLimit.set(newCacheEvictionLimit);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -19,6 +19,7 @@ import java.util.Collections;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.function.Function;
|
||||
|
||||
/**
|
||||
* <p>A basic OGNL LRU cache implementation.</p>
|
||||
@@ -54,6 +55,11 @@ public class OgnlLRUCache<K, V> implements OgnlCache<K, V> {
|
||||
return ognlLRUCache.get(key);
|
||||
}
|
||||
|
||||
@Override
|
||||
public V computeIfAbsent(K key, Function<? super K, ? extends V> mappingFunction) {
|
||||
return ognlLRUCache.computeIfAbsent(key, mappingFunction);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void put(K key, V value) {
|
||||
ognlLRUCache.put(key, value);
|
||||
|
||||
@@ -23,6 +23,10 @@ import ognl.Ognl;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* @deprecated since 6.8.0, to be removed, see {@link ReflectionContextFactory}
|
||||
*/
|
||||
@Deprecated(since = "6.8.0", forRemoval = true)
|
||||
public class OgnlReflectionContextFactory implements ReflectionContextFactory {
|
||||
|
||||
@Override
|
||||
|
||||
@@ -18,12 +18,6 @@
|
||||
*/
|
||||
package org.apache.struts2.ognl;
|
||||
|
||||
import org.apache.struts2.conversion.impl.XWorkConverter;
|
||||
import org.apache.struts2.inject.Container;
|
||||
import org.apache.struts2.inject.Inject;
|
||||
import org.apache.struts2.ognl.accessor.RootAccessor;
|
||||
import org.apache.struts2.util.CompoundRoot;
|
||||
import org.apache.struts2.util.reflection.ReflectionException;
|
||||
import ognl.ClassResolver;
|
||||
import ognl.Ognl;
|
||||
import ognl.OgnlContext;
|
||||
@@ -35,7 +29,12 @@ 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.ognl.OgnlGuard;
|
||||
import org.apache.struts2.conversion.impl.XWorkConverter;
|
||||
import org.apache.struts2.inject.Container;
|
||||
import org.apache.struts2.inject.Inject;
|
||||
import org.apache.struts2.ognl.accessor.RootAccessor;
|
||||
import org.apache.struts2.util.CompoundRoot;
|
||||
import org.apache.struts2.util.reflection.ReflectionException;
|
||||
|
||||
import java.beans.BeanInfo;
|
||||
import java.beans.IntrospectionException;
|
||||
@@ -676,13 +675,19 @@ public class OgnlUtil {
|
||||
* @throws IntrospectionException is thrown if an exception occurs during introspection.
|
||||
*/
|
||||
public BeanInfo getBeanInfo(Class<?> clazz) throws IntrospectionException {
|
||||
synchronized (beanInfoCache) {
|
||||
BeanInfo beanInfo = beanInfoCache.get(clazz);
|
||||
if (beanInfo == null) {
|
||||
beanInfo = Introspector.getBeanInfo(clazz, Object.class);
|
||||
beanInfoCache.putIfAbsent(clazz, beanInfo);
|
||||
try {
|
||||
return beanInfoCache.computeIfAbsent(clazz, k -> {
|
||||
try {
|
||||
return Introspector.getBeanInfo(k, Object.class);
|
||||
} catch (IntrospectionException e) {
|
||||
throw new IllegalArgumentException(e);
|
||||
}
|
||||
});
|
||||
} catch (IllegalArgumentException e) {
|
||||
if (e.getCause() instanceof IntrospectionException innerEx) {
|
||||
throw innerEx;
|
||||
}
|
||||
return beanInfo;
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -212,16 +212,17 @@ public class SecurityMemberAccess implements MemberAccess {
|
||||
return true;
|
||||
}
|
||||
|
||||
Class<?> targetClass = target != null ? target.getClass() : null;
|
||||
|
||||
if (!disallowProxyObjectAccess && ProxyUtil.isProxy(target)) {
|
||||
// If `disallowProxyObjectAccess` is not set, allow resolving Hibernate entities to their underlying
|
||||
// classes/members. This allows the allowlist capability to continue working and offer some level of
|
||||
// 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. This is preferred to having to disable the allowlist capability entirely.
|
||||
Object newTarget = ProxyUtil.getHibernateProxyTarget(target);
|
||||
if (newTarget != target) {
|
||||
logAllowlistHibernateEntity(target, newTarget);
|
||||
target = newTarget;
|
||||
member = ProxyUtil.resolveTargetMember(member, newTarget);
|
||||
// entities and Spring proxies. This is preferred to having to disable the allowlist capability entirely.
|
||||
Class<?> newTargetClass = ProxyUtil.ultimateTargetClass(target);
|
||||
if (newTargetClass != targetClass) {
|
||||
targetClass = newTargetClass;
|
||||
member = ProxyUtil.resolveTargetMember(member, newTargetClass);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -231,10 +232,10 @@ public class SecurityMemberAccess implements MemberAccess {
|
||||
memberClass, member, STRUTS_ALLOWLIST_CLASSES, STRUTS_ALLOWLIST_PACKAGE_NAMES);
|
||||
return false;
|
||||
}
|
||||
if (target == null || target.getClass() == memberClass) {
|
||||
|
||||
if (targetClass == null || targetClass == memberClass) {
|
||||
return true;
|
||||
}
|
||||
Class<?> targetClass = target.getClass();
|
||||
if (!isClassAllowlisted(targetClass)) {
|
||||
LOG.warn("Target class [{}] of target [{}] is not allowlisted! Add to '{}' or '{}' configuration.",
|
||||
targetClass, target, STRUTS_ALLOWLIST_CLASSES, STRUTS_ALLOWLIST_PACKAGE_NAMES);
|
||||
@@ -243,20 +244,6 @@ public class SecurityMemberAccess implements MemberAccess {
|
||||
return true;
|
||||
}
|
||||
|
||||
private void logAllowlistHibernateEntity(Object original, Object resolved) {
|
||||
if (!isDevMode && !LOG.isDebugEnabled()) {
|
||||
return;
|
||||
}
|
||||
String msg = "Hibernate entity [{}] resolved to [{}] for purpose of OGNL allowlisting." +
|
||||
" We don't recommend executing OGNL expressions against Hibernate entities, you may disallow this behaviour using the configuration `{}=true`.";
|
||||
Object[] args = {original, resolved, StrutsConstants.STRUTS_DISALLOW_PROXY_OBJECT_ACCESS};
|
||||
if (isDevMode) {
|
||||
LOG.warn(msg, args);
|
||||
} else {
|
||||
LOG.debug(msg, args);
|
||||
}
|
||||
}
|
||||
|
||||
protected boolean isClassAllowlisted(Class<?> clazz) {
|
||||
return allowlistClasses.contains(clazz)
|
||||
|| ALLOWLIST_REQUIRED_CLASSES.contains(clazz)
|
||||
|
||||
@@ -18,6 +18,8 @@
|
||||
*/
|
||||
package org.apache.struts2.ognl;
|
||||
|
||||
import org.apache.commons.lang3.ClassUtils;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
@@ -34,6 +36,15 @@ public class ThreadAllowlist {
|
||||
|
||||
private final ThreadLocal<Set<Class<?>>> allowlist = new ThreadLocal<>();
|
||||
|
||||
/**
|
||||
* @since 7.1.0
|
||||
*/
|
||||
public void allowClassHierarchy(Class<?> clazz) {
|
||||
allowClass(clazz);
|
||||
ClassUtils.getAllSuperclasses(clazz).forEach(this::allowClass);
|
||||
ClassUtils.getAllInterfaces(clazz).forEach(this::allowClass);
|
||||
}
|
||||
|
||||
public void allowClass(Class<?> clazz) {
|
||||
if (allowlist.get() == null) {
|
||||
allowlist.set(new HashSet<>());
|
||||
|
||||
@@ -18,8 +18,6 @@
|
||||
*/
|
||||
package org.apache.struts2.result;
|
||||
|
||||
import org.apache.struts2.ActionInvocation;
|
||||
import org.apache.struts2.inject.Inject;
|
||||
import jakarta.servlet.RequestDispatcher;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
@@ -28,9 +26,11 @@ import org.apache.commons.lang3.ObjectUtils;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.apache.struts2.ActionInvocation;
|
||||
import org.apache.struts2.ServletActionContext;
|
||||
import org.apache.struts2.StrutsStatics;
|
||||
import org.apache.struts2.dispatcher.HttpParameters;
|
||||
import org.apache.struts2.inject.Inject;
|
||||
import org.apache.struts2.url.QueryStringParser;
|
||||
|
||||
import java.io.Serial;
|
||||
@@ -158,13 +158,6 @@ public class ServletDispatcherResult extends StrutsResultSupport {
|
||||
//if we are inside an action tag, we always need to do an include
|
||||
boolean insideActionTag = (Boolean) ObjectUtils.defaultIfNull(request.getAttribute(StrutsStatics.STRUTS_ACTION_TAG_INVOCATION), Boolean.FALSE);
|
||||
|
||||
// this should allow integration with third-party view related frameworks
|
||||
if (finalLocation.contains("?")) {
|
||||
request.setAttribute(RequestDispatcher.FORWARD_SERVLET_PATH, finalLocation.substring(0, finalLocation.indexOf('?')));
|
||||
} else {
|
||||
request.setAttribute(RequestDispatcher.FORWARD_SERVLET_PATH, finalLocation);
|
||||
}
|
||||
|
||||
// If we're included, then include the view
|
||||
// Otherwise do forward
|
||||
// This allow the page to, for example, set content type
|
||||
@@ -176,6 +169,7 @@ public class ServletDispatcherResult extends StrutsResultSupport {
|
||||
dispatcher.forward(request, response);
|
||||
} else {
|
||||
LOG.debug("Including location: {}", finalLocation);
|
||||
|
||||
dispatcher.include(request, response);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,15 +26,19 @@ import org.apache.struts2.ognl.OgnlCache;
|
||||
import org.apache.struts2.ognl.OgnlCacheFactory;
|
||||
import org.hibernate.Hibernate;
|
||||
import org.hibernate.proxy.HibernateProxy;
|
||||
import org.springframework.aop.SpringProxy;
|
||||
import org.springframework.aop.TargetClassAware;
|
||||
import org.springframework.aop.framework.Advised;
|
||||
import org.springframework.aop.framework.AopProxyUtils;
|
||||
import org.springframework.aop.support.AopUtils;
|
||||
|
||||
import java.lang.reflect.Constructor;
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.Member;
|
||||
import java.lang.reflect.Method;
|
||||
import java.lang.reflect.Modifier;
|
||||
import java.lang.reflect.Proxy;
|
||||
|
||||
import static java.lang.reflect.Modifier.isPublic;
|
||||
import static java.lang.reflect.Modifier.isStatic;
|
||||
|
||||
/**
|
||||
* <code>ProxyUtil</code>
|
||||
@@ -44,17 +48,14 @@ import static java.lang.reflect.Modifier.isPublic;
|
||||
*
|
||||
*/
|
||||
public class ProxyUtil {
|
||||
private static final String SPRING_ADVISED_CLASS_NAME = "org.springframework.aop.framework.Advised";
|
||||
private static final String SPRING_SPRINGPROXY_CLASS_NAME = "org.springframework.aop.SpringProxy";
|
||||
private static final String SPRING_SINGLETONTARGETSOURCE_CLASS_NAME = "org.springframework.aop.target.SingletonTargetSource";
|
||||
private static final String SPRING_TARGETCLASSAWARE_CLASS_NAME = "org.springframework.aop.TargetClassAware";
|
||||
private static final String HIBERNATE_HIBERNATEPROXY_CLASS_NAME = "org.hibernate.proxy.HibernateProxy";
|
||||
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();
|
||||
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();
|
||||
|
||||
/**
|
||||
* Determine the ultimate target class of the given instance, traversing
|
||||
@@ -65,15 +66,18 @@ public class ProxyUtil {
|
||||
* object as fallback; never {@code null})
|
||||
*/
|
||||
public static Class<?> ultimateTargetClass(Object candidate) {
|
||||
Class<?> result = null;
|
||||
if (isSpringAopProxy(candidate))
|
||||
result = springUltimateTargetClass(candidate);
|
||||
|
||||
if (result == null) {
|
||||
result = candidate.getClass();
|
||||
}
|
||||
|
||||
return result;
|
||||
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;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -82,16 +86,8 @@ public class ProxyUtil {
|
||||
*/
|
||||
public static boolean isProxy(Object object) {
|
||||
if (object == null) return false;
|
||||
Class<?> clazz = object.getClass();
|
||||
Boolean flag = isProxyCache.get(clazz);
|
||||
if (flag != null) {
|
||||
return flag;
|
||||
}
|
||||
|
||||
boolean isProxy = isSpringAopProxy(object) || isHibernateProxy(object);
|
||||
|
||||
isProxyCache.put(clazz, isProxy);
|
||||
return isProxy;
|
||||
return isProxyCache.computeIfAbsent(object.getClass(),
|
||||
k -> isSpringAopProxy(object) || isHibernateProxy(object));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -100,19 +96,11 @@ public class ProxyUtil {
|
||||
* @param object the object to check
|
||||
*/
|
||||
public static boolean isProxyMember(Member member, Object object) {
|
||||
if (!Modifier.isStatic(member.getModifiers()) && !isProxy(object) && !isHibernateProxy(object)) {
|
||||
if (!isStatic(member.getModifiers()) && !isProxy(object)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
Boolean flag = isProxyMemberCache.get(member);
|
||||
if (flag != null) {
|
||||
return flag;
|
||||
}
|
||||
|
||||
boolean isProxyMember = isSpringProxyMember(member) || isHibernateProxyMember(member);
|
||||
|
||||
isProxyMemberCache.put(member, isProxyMember);
|
||||
return isProxyMember;
|
||||
return isProxyMemberCache.computeIfAbsent(member,
|
||||
k -> isSpringProxyMember(member) || isHibernateProxyMember(member));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -123,7 +111,7 @@ public class ProxyUtil {
|
||||
public static boolean isHibernateProxy(Object object) {
|
||||
try {
|
||||
return object != null && HibernateProxy.class.isAssignableFrom(object.getClass());
|
||||
} catch (NoClassDefFoundError ignored) {
|
||||
} catch (LinkageError ignored) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -135,12 +123,10 @@ public class ProxyUtil {
|
||||
*/
|
||||
public static boolean isHibernateProxyMember(Member member) {
|
||||
try {
|
||||
Class<?> clazz = ClassLoaderUtil.loadClass(HIBERNATE_HIBERNATEPROXY_CLASS_NAME, ProxyUtil.class);
|
||||
return hasMember(clazz, member);
|
||||
} catch (ClassNotFoundException ignored) {
|
||||
return hasMember(HibernateProxy.class, member);
|
||||
} catch (LinkageError ignored) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -152,20 +138,11 @@ public class ProxyUtil {
|
||||
* object as fallback; never {@code null})
|
||||
*/
|
||||
private static Class<?> springUltimateTargetClass(Object candidate) {
|
||||
Object current = candidate;
|
||||
Class<?> result = null;
|
||||
while (null != current && implementsInterface(current.getClass(), SPRING_TARGETCLASSAWARE_CLASS_NAME)) {
|
||||
try {
|
||||
result = (Class<?>) MethodUtils.invokeMethod(current, "getTargetClass");
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
current = getSingletonTarget(current);
|
||||
try {
|
||||
return AopProxyUtils.ultimateTargetClass(candidate);
|
||||
} catch (LinkageError ignored) {
|
||||
return candidate.getClass();
|
||||
}
|
||||
if (result == null) {
|
||||
Class<?> clazz = candidate.getClass();
|
||||
result = (isCglibProxyClass(clazz) ? clazz.getSuperclass() : candidate.getClass());
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -173,9 +150,11 @@ public class ProxyUtil {
|
||||
* @param object the object to check
|
||||
*/
|
||||
private static boolean isSpringAopProxy(Object object) {
|
||||
Class<?> clazz = object.getClass();
|
||||
return (implementsInterface(clazz, SPRING_SPRINGPROXY_CLASS_NAME) && (Proxy.isProxyClass(clazz)
|
||||
|| isCglibProxyClass(clazz)));
|
||||
try {
|
||||
return AopUtils.isAopProxy(object);
|
||||
} catch (LinkageError ignored) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -184,79 +163,32 @@ public class ProxyUtil {
|
||||
*/
|
||||
private static boolean isSpringProxyMember(Member member) {
|
||||
try {
|
||||
Class<?> clazz = ClassLoaderUtil.loadClass(SPRING_ADVISED_CLASS_NAME, ProxyUtil.class);
|
||||
if (hasMember(clazz, member))
|
||||
if (hasMember(Advised.class, member))
|
||||
return true;
|
||||
clazz = ClassLoaderUtil.loadClass(SPRING_TARGETCLASSAWARE_CLASS_NAME, ProxyUtil.class);
|
||||
if (hasMember(clazz, member))
|
||||
if (hasMember(TargetClassAware.class, member))
|
||||
return true;
|
||||
clazz = ClassLoaderUtil.loadClass(SPRING_SPRINGPROXY_CLASS_NAME, ProxyUtil.class);
|
||||
if (hasMember(clazz, member))
|
||||
if (hasMember(SpringProxy.class, member))
|
||||
return true;
|
||||
} catch (ClassNotFoundException ignored) {
|
||||
} catch (LinkageError ignored) {
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtain the singleton target object behind the given spring proxy, if any.
|
||||
* @param candidate the (potential) spring proxy to check
|
||||
* @return the singleton target object, or {@code null} in any other case
|
||||
* (not a spring proxy, not an existing singleton target)
|
||||
*/
|
||||
private static Object getSingletonTarget(Object candidate) {
|
||||
try {
|
||||
if (implementsInterface(candidate.getClass(), SPRING_ADVISED_CLASS_NAME)) {
|
||||
Object targetSource = MethodUtils.invokeMethod(candidate, "getTargetSource");
|
||||
if (implementsInterface(targetSource.getClass(), SPRING_SINGLETONTARGETSOURCE_CLASS_NAME)) {
|
||||
return MethodUtils.invokeMethod(targetSource, "getTarget");
|
||||
}
|
||||
}
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether the specified class is a CGLIB-generated class.
|
||||
* @param clazz the class to check
|
||||
*/
|
||||
private static boolean isCglibProxyClass(Class<?> clazz) {
|
||||
return (clazz != null && clazz.getName().contains("$$"));
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether the given class implements an interface with a given class name.
|
||||
* @param clazz the class to check
|
||||
* @param ifaceClassName the interface class name to check
|
||||
*/
|
||||
private static boolean implementsInterface(Class<?> clazz, String ifaceClassName) {
|
||||
try {
|
||||
Class<?> ifaceClass = ClassLoaderUtil.loadClass(ifaceClassName, ProxyUtil.class);
|
||||
return ifaceClass.isAssignableFrom(clazz);
|
||||
} catch (ClassNotFoundException e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether the given class has a given member.
|
||||
* @param clazz the class to check
|
||||
* @param member the member to check
|
||||
*/
|
||||
private static boolean hasMember(Class<?> clazz, Member member) {
|
||||
if (member instanceof Method) {
|
||||
return null != MethodUtils.getMatchingMethod(clazz, member.getName(), ((Method) member).getParameterTypes());
|
||||
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) {
|
||||
return null != ConstructorUtils.getMatchingAccessibleConstructor(clazz, ((Constructor) member).getParameterTypes());
|
||||
if (member instanceof Constructor<?> constructor) {
|
||||
return null != ConstructorUtils.getMatchingAccessibleConstructor(clazz, constructor.getParameterTypes());
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -266,26 +198,34 @@ public class ProxyUtil {
|
||||
public static Object getHibernateProxyTarget(Object object) {
|
||||
try {
|
||||
return Hibernate.unproxy(object);
|
||||
} catch (NoClassDefFoundError ignored) {
|
||||
} catch (LinkageError ignored) {
|
||||
return object;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated since 7.1, use {@link #resolveTargetMember(Member, Class)} instead.
|
||||
*/
|
||||
@Deprecated
|
||||
public static Member resolveTargetMember(Member proxyMember, Object target) {
|
||||
return resolveTargetMember(proxyMember, target.getClass());
|
||||
}
|
||||
|
||||
/**
|
||||
* @return matching member on target object if one exists, otherwise the same member
|
||||
*/
|
||||
public static Member resolveTargetMember(Member proxyMember, Object target) {
|
||||
public static Member resolveTargetMember(Member proxyMember, Class<?> targetClass) {
|
||||
int mod = proxyMember.getModifiers();
|
||||
if (proxyMember instanceof Method) {
|
||||
if (isPublic(mod)) {
|
||||
return MethodUtils.getMatchingAccessibleMethod(target.getClass(), proxyMember.getName(), ((Method) proxyMember).getParameterTypes());
|
||||
return MethodUtils.getMatchingAccessibleMethod(targetClass, proxyMember.getName(), ((Method) proxyMember).getParameterTypes());
|
||||
} else {
|
||||
return MethodUtils.getMatchingMethod(target.getClass(), proxyMember.getName(), ((Method) proxyMember).getParameterTypes());
|
||||
return MethodUtils.getMatchingMethod(targetClass, proxyMember.getName(), ((Method) proxyMember).getParameterTypes());
|
||||
}
|
||||
} else if (proxyMember instanceof Field) {
|
||||
return FieldUtils.getField(target.getClass(), proxyMember.getName(), isPublic(mod));
|
||||
return FieldUtils.getField(targetClass, proxyMember.getName(), isPublic(mod));
|
||||
} else if (proxyMember instanceof Constructor && isPublic(mod)) {
|
||||
return ConstructorUtils.getMatchingAccessibleConstructor(target.getClass(), ((Constructor<?>) proxyMember).getParameterTypes());
|
||||
return ConstructorUtils.getMatchingAccessibleConstructor(targetClass, ((Constructor<?>) proxyMember).getParameterTypes());
|
||||
}
|
||||
return proxyMember;
|
||||
}
|
||||
|
||||
@@ -20,6 +20,10 @@ package org.apache.struts2.util.reflection;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* @deprecated since 6.8.0, avoid using this interface and any of its implementation, it's going to be removed soon
|
||||
*/
|
||||
@Deprecated(since = "6.8.0", forRemoval = true)
|
||||
public interface ReflectionContextFactory {
|
||||
/**
|
||||
* Creates and returns a new standard naming context for evaluating an OGNL
|
||||
|
||||
@@ -299,4 +299,7 @@ struts.url.queryStringParser=strutsQueryStringParser
|
||||
struts.url.encoder=strutsUrlEncoder
|
||||
struts.url.decoder=strutsUrlDecoder
|
||||
|
||||
### Defines source to read nonce value from, possible values are: request, session
|
||||
struts.csp.nonceSource=session
|
||||
|
||||
### END SNIPPET: complete_file
|
||||
|
||||
@@ -75,6 +75,11 @@ struts.messages.upload.error.FileUploadContentTypeException=Request has wrong co
|
||||
# Default error message when handling multi-part request
|
||||
struts.messages.upload.error.FileUploadException=Error parsing the multi-part request.
|
||||
|
||||
# IllegalArgumentException for empty files
|
||||
# 0 - original filename
|
||||
# 1 - field name
|
||||
struts.messages.upload.error.IllegalArgumentException=File {0} for field {1} is empty (0 bytes). Empty file uploads are not allowed.
|
||||
|
||||
devmode.notification=Developer Notification (set struts.devMode to false to disable this message):\n{0}
|
||||
|
||||
struts.exception.missing-package-action.with-context = There is no Action mapped for namespace [{0}] and action name [{1}] associated with context path [{2}].
|
||||
|
||||
@@ -250,4 +250,7 @@
|
||||
<bean type="org.apache.struts2.interceptor.exec.ExecutorProvider" name="struts"
|
||||
class="org.apache.struts2.interceptor.exec.StrutsExecutorProvider"/>
|
||||
|
||||
<bean type="org.apache.struts2.interceptor.csp.CspNonceReader" name="struts"
|
||||
class="org.apache.struts2.interceptor.csp.StrutsCspNonceReader"/>
|
||||
|
||||
</struts>
|
||||
|
||||
@@ -165,14 +165,6 @@
|
||||
<td class="tag-attribute">String</td>
|
||||
<td class="tag-attribute">Set the value used to retrieve the option value.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="tag-attribute">maxLength</td>
|
||||
<td class="tag-attribute">false</td>
|
||||
<td class="tag-attribute"></td>
|
||||
<td class="tag-attribute">false</td>
|
||||
<td class="tag-attribute">Integer</td>
|
||||
<td class="tag-attribute">Deprecated. Use maxlength instead.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="tag-attribute">maxlength</td>
|
||||
<td class="tag-attribute">false</td>
|
||||
|
||||
@@ -117,14 +117,6 @@
|
||||
<td class="tag-attribute">String</td>
|
||||
<td class="tag-attribute">String that will be appended to the label</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="tag-attribute">maxLength</td>
|
||||
<td class="tag-attribute">false</td>
|
||||
<td class="tag-attribute"></td>
|
||||
<td class="tag-attribute">false</td>
|
||||
<td class="tag-attribute">Integer</td>
|
||||
<td class="tag-attribute">Deprecated. Use maxlength instead.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="tag-attribute">maxlength</td>
|
||||
<td class="tag-attribute">false</td>
|
||||
|
||||
@@ -117,14 +117,6 @@
|
||||
<td class="tag-attribute">String</td>
|
||||
<td class="tag-attribute">String that will be appended to the label</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="tag-attribute">maxLength</td>
|
||||
<td class="tag-attribute">false</td>
|
||||
<td class="tag-attribute"></td>
|
||||
<td class="tag-attribute">false</td>
|
||||
<td class="tag-attribute">Integer</td>
|
||||
<td class="tag-attribute">Deprecated. Use maxlength instead.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="tag-attribute">maxlength</td>
|
||||
<td class="tag-attribute">false</td>
|
||||
|
||||
@@ -21,6 +21,7 @@ package org.apache.struts2.components;
|
||||
import org.apache.struts2.ActionContext;
|
||||
import org.apache.struts2.config.ConfigurationException;
|
||||
import org.apache.struts2.util.ValueStack;
|
||||
import org.apache.struts2.StrutsConstants;
|
||||
import org.apache.struts2.StrutsInternalTestCase;
|
||||
import org.apache.struts2.components.template.Template;
|
||||
import org.apache.struts2.components.template.TemplateEngine;
|
||||
@@ -32,6 +33,7 @@ import org.springframework.mock.web.MockHttpServletResponse;
|
||||
import org.springframework.mock.web.MockHttpSession;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.apache.struts2.security.DefaultNotExcludedAcceptedPatternsCheckerTest.NO_EXCLUSION_ACCEPT_ALL_PATTERNS_CHECKER;
|
||||
@@ -236,6 +238,8 @@ public class UIBeanTest extends StrutsInternalTestCase {
|
||||
ActionContext.getContext().withServletRequest(req);
|
||||
|
||||
TextField txtFld = new TextField(stack, req, res);
|
||||
container.inject(txtFld);
|
||||
|
||||
txtFld.setAccesskey(accesskeyValue);
|
||||
txtFld.evaluateParams();
|
||||
|
||||
@@ -250,6 +254,8 @@ public class UIBeanTest extends StrutsInternalTestCase {
|
||||
ActionContext.getContext().withServletRequest(req);
|
||||
|
||||
TextField txtFld = new TextField(stack, req, res);
|
||||
container.inject(txtFld);
|
||||
|
||||
txtFld.addParameter("value", value);
|
||||
txtFld.evaluateParams();
|
||||
|
||||
@@ -338,6 +344,8 @@ public class UIBeanTest extends StrutsInternalTestCase {
|
||||
ActionContext.getContext().withServletRequest(req);
|
||||
|
||||
TextField txtFld = new TextField(stack, req, res);
|
||||
container.inject(txtFld);
|
||||
|
||||
txtFld.setCssClass(cssClass);
|
||||
txtFld.evaluateParams();
|
||||
|
||||
@@ -352,6 +360,8 @@ public class UIBeanTest extends StrutsInternalTestCase {
|
||||
ActionContext.getContext().withServletRequest(req);
|
||||
|
||||
TextField txtFld = new TextField(stack, req, res);
|
||||
container.inject(txtFld);
|
||||
|
||||
txtFld.setStyle(cssStyle);
|
||||
txtFld.evaluateParams();
|
||||
|
||||
@@ -372,6 +382,8 @@ public class UIBeanTest extends StrutsInternalTestCase {
|
||||
actionContext.withSession(new SessionMap(req));
|
||||
|
||||
DoubleSelect dblSelect = new DoubleSelect(stack, req, res);
|
||||
container.inject(dblSelect);
|
||||
|
||||
dblSelect.evaluateParams();
|
||||
|
||||
assertEquals(nonceVal, dblSelect.getAttributes().get("nonce"));
|
||||
@@ -392,11 +404,35 @@ public class UIBeanTest extends StrutsInternalTestCase {
|
||||
session.invalidate();
|
||||
|
||||
DoubleSelect dblSelect = new DoubleSelect(stack, req, res);
|
||||
container.inject(dblSelect);
|
||||
|
||||
dblSelect.evaluateParams();
|
||||
|
||||
assertNull(dblSelect.getAttributes().get("nonce"));
|
||||
}
|
||||
|
||||
public void testNonceOfRequestAttribute() {
|
||||
Map<String, String> params = new HashMap<String, String>(){{
|
||||
put(StrutsConstants.STRUTS_CSP_NONCE_SOURCE, "request");
|
||||
}};
|
||||
initDispatcher(params);
|
||||
|
||||
String nonceVal = "r4nd0m";
|
||||
ValueStack stack = ActionContext.getContext().getValueStack();
|
||||
MockHttpServletRequest req = new MockHttpServletRequest();
|
||||
req.setAttribute("nonce", nonceVal);
|
||||
MockHttpServletResponse res = new MockHttpServletResponse();
|
||||
ActionContext actionContext = stack.getActionContext();
|
||||
actionContext.withServletRequest(req);
|
||||
|
||||
DoubleSelect dblSelect = new DoubleSelect(stack, req, res);
|
||||
container.inject(dblSelect);
|
||||
|
||||
dblSelect.evaluateParams();
|
||||
|
||||
assertEquals(nonceVal, dblSelect.getAttributes().get("nonce"));
|
||||
}
|
||||
|
||||
public void testSetNullUiStaticContentPath() {
|
||||
// given
|
||||
ValueStack stack = ActionContext.getContext().getValueStack();
|
||||
|
||||
@@ -50,6 +50,11 @@ import java.util.*;
|
||||
|
||||
import static org.junit.Assert.assertArrayEquals;
|
||||
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.LocalTime;
|
||||
import java.time.OffsetDateTime;
|
||||
import java.util.Date;
|
||||
import java.util.Set;
|
||||
|
||||
@@ -100,6 +105,36 @@ public class XWorkConverterTest extends XWorkTestCase {
|
||||
|
||||
Date dateRfc3339DateOnly = (Date) converter.convertValue(context, null, null, null, "2001-01-10", Date.class);
|
||||
assertEquals(date, dateRfc3339DateOnly);
|
||||
|
||||
// java.time library tests
|
||||
DateTimeFormatter formatterDate = DateTimeFormatter.ofPattern("MM/dd/yyyy");
|
||||
LocalDate localDate = LocalDate.parse("01/10/2001", formatterDate);
|
||||
|
||||
DateTimeFormatter formatterDateTime = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss");
|
||||
LocalDateTime localDateTime = LocalDateTime.parse("2001-01-10T10:11:12", formatterDateTime);
|
||||
|
||||
DateTimeFormatter formatterTime = DateTimeFormatter.ofPattern("HH:mm:ss");
|
||||
LocalTime localTime = LocalTime.parse("10:11:12", formatterTime);
|
||||
|
||||
DateTimeFormatter formatterOffsetDateTime = DateTimeFormatter.ISO_OFFSET_DATE_TIME;
|
||||
OffsetDateTime offsetDateTime = OffsetDateTime.parse("2001-01-10T10:11:12+05:00", formatterOffsetDateTime);
|
||||
|
||||
String localDateStr = (String) converter.convertValue(context, null, null, null, localDate, String.class);
|
||||
String localDateTimeStr = (String) converter.convertValue(context, null, null, null, localDateTime, String.class);
|
||||
String localTimeStr = (String) converter.convertValue(context, null, null, null, localTime, String.class);
|
||||
String offsetDateTimeStr = (String) converter.convertValue(context, null, null, null, offsetDateTime, String.class);
|
||||
|
||||
LocalDate localDate2 = (LocalDate) converter.convertValue(context, null, null, null, localDateStr, LocalDate.class);
|
||||
assertEquals(localDate, localDate2);
|
||||
|
||||
LocalDateTime localDateTime2 = (LocalDateTime) converter.convertValue(context, null, null, null, localDateTimeStr, LocalDateTime.class);
|
||||
assertEquals(localDateTime, localDateTime2);
|
||||
|
||||
LocalTime localTime2 = (LocalTime) converter.convertValue(context, null, null, null, localTimeStr, LocalTime.class);
|
||||
assertEquals(localTime, localTime2);
|
||||
|
||||
OffsetDateTime offsetDateTime2 = (OffsetDateTime) converter.convertValue(context, null, null, null, offsetDateTimeStr, OffsetDateTime.class);
|
||||
assertEquals(offsetDateTime, offsetDateTime2);
|
||||
}
|
||||
|
||||
public void testDateConversionWithDefault() throws ParseException {
|
||||
|
||||
+365
-1
@@ -30,6 +30,9 @@ import org.springframework.mock.web.MockHttpServletRequest;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
@@ -417,7 +420,6 @@ abstract class AbstractMultiPartRequestTest {
|
||||
assertThat(JakartaServletDiskFileUpload.isMultipartContent(mockRequest)).isTrue();
|
||||
|
||||
// when
|
||||
mockRequest.setCharacterEncoding(null);
|
||||
multiPart.setDefaultEncoding(StandardCharsets.ISO_8859_1.name());
|
||||
multiPart.parse(mockRequest, tempDir);
|
||||
|
||||
@@ -492,6 +494,368 @@ abstract class AbstractMultiPartRequestTest {
|
||||
.containsExactly("struts.messages.upload.error.FileUploadException");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void cleanupDoesNotClearErrorsList() throws IOException {
|
||||
// given - create a scenario that generates errors
|
||||
String content = formFile("file1", "test1.csv", "1,2,3,4");
|
||||
mockRequest.setContent(content.getBytes(StandardCharsets.UTF_8));
|
||||
|
||||
multiPart.setMaxSize("1"); // Very small to trigger error
|
||||
multiPart.parse(mockRequest, tempDir);
|
||||
|
||||
// Verify errors exist
|
||||
assertThat(multiPart.getErrors()).isNotEmpty();
|
||||
int originalErrorCount = multiPart.getErrors().size();
|
||||
|
||||
// when
|
||||
multiPart.cleanUp();
|
||||
|
||||
// then - errors should remain (cleanup doesn't clear errors)
|
||||
assertThat(multiPart.getErrors()).hasSize(originalErrorCount);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void largeFileUploadHandling() throws IOException {
|
||||
// Test that large files are handled properly
|
||||
StringBuilder largeContent = new StringBuilder();
|
||||
for (int i = 0; i < 1000; i++) {
|
||||
largeContent.append("line").append(i).append(",");
|
||||
}
|
||||
|
||||
String content = formFile("largefile", "large.csv", largeContent.toString()) +
|
||||
endline + "--" + boundary + "--";
|
||||
|
||||
mockRequest.setContent(content.getBytes(StandardCharsets.UTF_8));
|
||||
|
||||
// when
|
||||
multiPart.parse(mockRequest, tempDir);
|
||||
|
||||
// then - should complete without memory issues
|
||||
assertThat(multiPart.getErrors()).isEmpty();
|
||||
assertThat(multiPart.getFile("largefile")).hasSize(1);
|
||||
|
||||
// Cleanup should properly handle large files
|
||||
multiPart.cleanUp();
|
||||
assertThat(multiPart.uploadedFiles).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void multipleFileUploadWithMixedContent() throws IOException {
|
||||
// Test mixed content with multiple files and parameters
|
||||
String content = formFile("file1", "test1.csv", "1,2,3,4") +
|
||||
formField("param1", "value1") +
|
||||
formFile("file2", "test2.csv", "5,6,7,8") +
|
||||
formField("param2", "value2") +
|
||||
formFile("file3", "test3.csv", "9,10,11,12") +
|
||||
formField("param3", "value3") +
|
||||
endline + "--" + boundary + "--";
|
||||
|
||||
mockRequest.setContent(content.getBytes(StandardCharsets.UTF_8));
|
||||
|
||||
// when
|
||||
multiPart.parse(mockRequest, tempDir);
|
||||
|
||||
// then - verify all content was processed
|
||||
assertThat(multiPart.getErrors()).isEmpty();
|
||||
assertThat(multiPart.getFile("file1")).hasSize(1);
|
||||
assertThat(multiPart.getFile("file2")).hasSize(1);
|
||||
assertThat(multiPart.getFile("file3")).hasSize(1);
|
||||
assertThat(multiPart.getParameter("param1")).isEqualTo("value1");
|
||||
assertThat(multiPart.getParameter("param2")).isEqualTo("value2");
|
||||
assertThat(multiPart.getParameter("param3")).isEqualTo("value3");
|
||||
|
||||
// Store file paths for post-cleanup verification
|
||||
List<String> filePaths = new ArrayList<>();
|
||||
for (UploadedFile file : multiPart.getFile("file1")) {
|
||||
filePaths.add(file.getAbsolutePath());
|
||||
}
|
||||
for (UploadedFile file : multiPart.getFile("file2")) {
|
||||
filePaths.add(file.getAbsolutePath());
|
||||
}
|
||||
for (UploadedFile file : multiPart.getFile("file3")) {
|
||||
filePaths.add(file.getAbsolutePath());
|
||||
}
|
||||
|
||||
// when - cleanup
|
||||
multiPart.cleanUp();
|
||||
|
||||
// then - verify complete cleanup
|
||||
assertThat(multiPart.uploadedFiles).isEmpty();
|
||||
assertThat(multiPart.parameters).isEmpty();
|
||||
|
||||
// Verify files are deleted
|
||||
for (String filePath : filePaths) {
|
||||
assertThat(new File(filePath)).doesNotExist();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void createTemporaryFileGeneratesSecureNames() {
|
||||
// Create a test instance to access the protected method
|
||||
AbstractMultiPartRequest testRequest = createMultipartRequest();
|
||||
Path testLocation = Paths.get(tempDir);
|
||||
|
||||
// when - create multiple temporary files
|
||||
File tempFile1 = testRequest.createTemporaryFile("test1.csv", testLocation);
|
||||
File tempFile2 = testRequest.createTemporaryFile("test2.csv", testLocation);
|
||||
File tempFile3 = testRequest.createTemporaryFile("../../../malicious.csv", testLocation);
|
||||
|
||||
// then - verify secure naming
|
||||
assertThat(tempFile1.getName()).startsWith("upload_");
|
||||
assertThat(tempFile1.getName()).endsWith(".tmp");
|
||||
assertThat(tempFile2.getName()).startsWith("upload_");
|
||||
assertThat(tempFile2.getName()).endsWith(".tmp");
|
||||
assertThat(tempFile3.getName()).startsWith("upload_");
|
||||
assertThat(tempFile3.getName()).endsWith(".tmp");
|
||||
|
||||
// Verify each file has a unique name
|
||||
assertThat(tempFile1.getName()).isNotEqualTo(tempFile2.getName());
|
||||
assertThat(tempFile2.getName()).isNotEqualTo(tempFile3.getName());
|
||||
assertThat(tempFile1.getName()).isNotEqualTo(tempFile3.getName());
|
||||
|
||||
// Verify all files are in the correct location
|
||||
assertThat(tempFile1.getParent()).isEqualTo(tempDir);
|
||||
assertThat(tempFile2.getParent()).isEqualTo(tempDir);
|
||||
assertThat(tempFile3.getParent()).isEqualTo(tempDir);
|
||||
|
||||
// Verify malicious filename doesn't affect the location
|
||||
assertThat(tempFile3.getName()).doesNotContain("..");
|
||||
assertThat(tempFile3.getName()).doesNotContain("/");
|
||||
assertThat(tempFile3.getName()).doesNotContain("\\");
|
||||
|
||||
// Clean up test files
|
||||
tempFile1.delete();
|
||||
tempFile2.delete();
|
||||
tempFile3.delete();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void createTemporaryFileInSpecificDirectory() throws IOException {
|
||||
// Create a subdirectory for testing
|
||||
Path subDir = Paths.get(tempDir, "subdir");
|
||||
Files.createDirectories(subDir);
|
||||
|
||||
AbstractMultiPartRequest testRequest = createMultipartRequest();
|
||||
|
||||
// when
|
||||
File tempFile = testRequest.createTemporaryFile("test.csv", subDir);
|
||||
|
||||
// then - verify file is created in the specified subdirectory
|
||||
assertThat(tempFile.getParent()).isEqualTo(subDir.toString());
|
||||
assertThat(tempFile.getName()).startsWith("upload_");
|
||||
assertThat(tempFile.getName()).endsWith(".tmp");
|
||||
|
||||
// Clean up
|
||||
tempFile.delete();
|
||||
Files.delete(subDir);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void createTemporaryFileWithNullFileName() throws IOException {
|
||||
AbstractMultiPartRequest testRequest = createMultipartRequest();
|
||||
Path testLocation = Paths.get(tempDir);
|
||||
|
||||
// when - create temp file with null filename
|
||||
File tempFile = testRequest.createTemporaryFile(null, testLocation);
|
||||
|
||||
// then - should still create a valid temporary file
|
||||
assertThat(tempFile.getName()).startsWith("upload_");
|
||||
assertThat(tempFile.getName()).endsWith(".tmp");
|
||||
assertThat(tempFile.getParent()).isEqualTo(tempDir);
|
||||
|
||||
// Clean up
|
||||
tempFile.delete();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void createTemporaryFileWithEmptyFileName() throws IOException {
|
||||
AbstractMultiPartRequest testRequest = createMultipartRequest();
|
||||
Path testLocation = Paths.get(tempDir);
|
||||
|
||||
// when - create temp file with empty filename
|
||||
File tempFile = testRequest.createTemporaryFile("", testLocation);
|
||||
|
||||
// then - should still create a valid temporary file
|
||||
assertThat(tempFile.getName()).startsWith("upload_");
|
||||
assertThat(tempFile.getName()).endsWith(".tmp");
|
||||
assertThat(tempFile.getParent()).isEqualTo(tempDir);
|
||||
|
||||
// Clean up
|
||||
tempFile.delete();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void createTemporaryFileWithSpecialCharacters() {
|
||||
AbstractMultiPartRequest testRequest = createMultipartRequest();
|
||||
Path testLocation = Paths.get(tempDir);
|
||||
|
||||
// when - create temp files with various special characters
|
||||
File tempFile1 = testRequest.createTemporaryFile("file with spaces.csv", testLocation);
|
||||
File tempFile2 = testRequest.createTemporaryFile("file@#$%^&*().csv", testLocation);
|
||||
File tempFile3 = testRequest.createTemporaryFile("файл.csv", testLocation); // Cyrillic
|
||||
|
||||
// then - all should create valid secure temporary files
|
||||
File[] tempFiles = {tempFile1, tempFile2, tempFile3};
|
||||
for (File tempFile : tempFiles) {
|
||||
assertThat(tempFile.getName()).startsWith("upload_");
|
||||
assertThat(tempFile.getName()).endsWith(".tmp");
|
||||
assertThat(tempFile.getParent()).isEqualTo(tempDir);
|
||||
// Verify no special characters leak into the actual filename
|
||||
assertThat(tempFile.getName()).matches("upload_[a-zA-Z0-9_]+\\.tmp");
|
||||
}
|
||||
|
||||
// All should have unique names
|
||||
assertThat(tempFile1.getName()).isNotEqualTo(tempFile2.getName());
|
||||
assertThat(tempFile2.getName()).isNotEqualTo(tempFile3.getName());
|
||||
assertThat(tempFile1.getName()).isNotEqualTo(tempFile3.getName());
|
||||
|
||||
// Clean up
|
||||
tempFile1.delete();
|
||||
tempFile2.delete();
|
||||
tempFile3.delete();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void createTemporaryFileConsistentNaming() {
|
||||
AbstractMultiPartRequest testRequest = createMultipartRequest();
|
||||
Path testLocation = Paths.get(tempDir);
|
||||
|
||||
// when - create many temporary files to verify naming consistency
|
||||
List<File> tempFiles = new ArrayList<>();
|
||||
for (int i = 0; i < 100; i++) {
|
||||
tempFiles.add(testRequest.createTemporaryFile("test" + i + ".csv", testLocation));
|
||||
}
|
||||
|
||||
// then - all should follow the same naming pattern
|
||||
for (File tempFile : tempFiles) {
|
||||
assertThat(tempFile.getName()).startsWith("upload_");
|
||||
assertThat(tempFile.getName()).endsWith(".tmp");
|
||||
assertThat(tempFile.getParent()).isEqualTo(tempDir);
|
||||
// Verify UUID pattern (without hyphens, replaced with underscores)
|
||||
assertThat(tempFile.getName()).matches("upload_[a-zA-Z0-9_]+\\.tmp");
|
||||
}
|
||||
|
||||
// Verify all names are unique
|
||||
List<String> fileNames = tempFiles.stream().map(File::getName).toList();
|
||||
assertThat(fileNames).doesNotHaveDuplicates();
|
||||
|
||||
// Clean up
|
||||
tempFiles.forEach(File::delete);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void emptyFileUploadsAreRejected() throws IOException {
|
||||
// Test that empty files (0 bytes) are rejected with proper error message
|
||||
String content =
|
||||
endline + "--" + boundary + endline +
|
||||
"Content-Disposition: form-data; name=\"emptyfile\"; filename=\"empty.txt\"" + endline +
|
||||
"Content-Type: text/plain" + endline +
|
||||
endline +
|
||||
// No content - this creates a 0-byte file
|
||||
endline + "--" + boundary + "--";
|
||||
|
||||
mockRequest.setContent(content.getBytes(StandardCharsets.UTF_8));
|
||||
|
||||
// when
|
||||
multiPart.parse(mockRequest, tempDir);
|
||||
|
||||
// then - should reject empty file and add error
|
||||
assertThat(multiPart.getErrors())
|
||||
.hasSize(1)
|
||||
.first()
|
||||
.satisfies(error -> {
|
||||
assertThat(error.getTextKey()).isEqualTo("struts.messages.upload.error.IllegalArgumentException");
|
||||
assertThat(error.getArgs()).containsExactly("empty.txt", "emptyfile");
|
||||
});
|
||||
assertThat(multiPart.uploadedFiles).isEmpty();
|
||||
assertThat(multiPart.getFile("emptyfile")).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void mixedEmptyAndValidFilesProcessedCorrectly() throws IOException {
|
||||
// Test that valid files are processed while empty files are rejected
|
||||
String content =
|
||||
endline + "--" + boundary + endline +
|
||||
"Content-Disposition: form-data; name=\"emptyfile1\"; filename=\"empty1.txt\"" + endline +
|
||||
"Content-Type: text/plain" + endline +
|
||||
endline +
|
||||
// No content - empty file
|
||||
endline + "--" + boundary + endline +
|
||||
"Content-Disposition: form-data; name=\"validfile\"; filename=\"valid.txt\"" + endline +
|
||||
"Content-Type: text/plain" + endline +
|
||||
endline +
|
||||
"some valid content" +
|
||||
endline + "--" + boundary + endline +
|
||||
"Content-Disposition: form-data; name=\"emptyfile2\"; filename=\"empty2.txt\"" + endline +
|
||||
"Content-Type: application/octet-stream" + endline +
|
||||
endline +
|
||||
// Another empty file
|
||||
endline + "--" + boundary + "--";
|
||||
|
||||
mockRequest.setContent(content.getBytes(StandardCharsets.UTF_8));
|
||||
|
||||
// when
|
||||
multiPart.parse(mockRequest, tempDir);
|
||||
|
||||
// then - should have 2 errors for empty files, 1 valid file processed
|
||||
assertThat(multiPart.getErrors()).hasSize(2);
|
||||
assertThat(multiPart.getErrors().get(0))
|
||||
.satisfies(error -> {
|
||||
assertThat(error.getTextKey()).isEqualTo("struts.messages.upload.error.IllegalArgumentException");
|
||||
assertThat(error.getArgs()).containsExactly("empty1.txt", "emptyfile1");
|
||||
});
|
||||
assertThat(multiPart.getErrors().get(1))
|
||||
.satisfies(error -> {
|
||||
assertThat(error.getTextKey()).isEqualTo("struts.messages.upload.error.IllegalArgumentException");
|
||||
assertThat(error.getArgs()).containsExactly("empty2.txt", "emptyfile2");
|
||||
});
|
||||
|
||||
// Only the valid file should be processed
|
||||
assertThat(multiPart.uploadedFiles).hasSize(1);
|
||||
assertThat(multiPart.getFile("validfile")).hasSize(1);
|
||||
assertThat(multiPart.getFile("emptyfile1")).isEmpty();
|
||||
assertThat(multiPart.getFile("emptyfile2")).isEmpty();
|
||||
|
||||
// Verify valid file content
|
||||
assertThat(multiPart.getFile("validfile")[0].getContent())
|
||||
.asInstanceOf(InstanceOfAssertFactories.FILE)
|
||||
.content()
|
||||
.isEqualTo("some valid content");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void emptyFileTemporaryFileCleanup() throws IOException {
|
||||
// Test that temporary files for empty files are properly cleaned up
|
||||
String content =
|
||||
endline + "--" + boundary + endline +
|
||||
"Content-Disposition: form-data; name=\"emptyfile\"; filename=\"empty.txt\"" + endline +
|
||||
"Content-Type: text/plain" + endline +
|
||||
endline +
|
||||
// Empty file
|
||||
endline + "--" + boundary + "--";
|
||||
|
||||
mockRequest.setContent(content.getBytes(StandardCharsets.UTF_8));
|
||||
|
||||
// Count temp files before processing
|
||||
File[] tempFilesBefore = new File(tempDir).listFiles((dir, name) -> name.startsWith("upload_") && name.endsWith(".tmp"));
|
||||
int countBefore = tempFilesBefore != null ? tempFilesBefore.length : 0;
|
||||
|
||||
// when
|
||||
multiPart.parse(mockRequest, tempDir);
|
||||
|
||||
// then - should reject empty file and clean up temp file
|
||||
assertThat(multiPart.getErrors()).hasSize(1);
|
||||
assertThat(multiPart.uploadedFiles).isEmpty();
|
||||
|
||||
// Verify that temporary files are cleaned up (may have implementation differences)
|
||||
// Some implementations create temp files first, others don't create any for empty uploads
|
||||
File[] tempFilesAfter = new File(tempDir).listFiles((dir, name) -> name.startsWith("upload_") && name.endsWith(".tmp"));
|
||||
int countAfter = tempFilesAfter != null ? tempFilesAfter.length : 0;
|
||||
|
||||
// Allow for implementation differences - just ensure no new temp files remain
|
||||
assertThat(countAfter).isLessThanOrEqualTo(countBefore);
|
||||
}
|
||||
|
||||
protected String formFile(String fieldName, String filename, String content) {
|
||||
return endline +
|
||||
"--" + boundary + endline +
|
||||
|
||||
+428
@@ -18,6 +18,22 @@
|
||||
*/
|
||||
package org.apache.struts2.dispatcher.multipart;
|
||||
|
||||
import org.apache.commons.fileupload2.core.DiskFileItem;
|
||||
import org.apache.struts2.dispatcher.LocalizedMessage;
|
||||
import org.assertj.core.api.InstanceOfAssertFactories;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.lang.reflect.Field;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
|
||||
import static org.apache.commons.lang3.StringUtils.normalizeSpace;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
public class JakartaMultiPartRequestTest extends AbstractMultiPartRequestTest {
|
||||
|
||||
@Override
|
||||
@@ -25,4 +41,416 @@ public class JakartaMultiPartRequestTest extends AbstractMultiPartRequestTest {
|
||||
return new JakartaMultiPartRequest();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void temporaryFileCleanupForInMemoryUploads() throws IOException, NoSuchFieldException, IllegalAccessException {
|
||||
// given - small files that will be in-memory
|
||||
String content = formFile("file1", "test1.csv", "a,b,c,d") +
|
||||
formFile("file2", "test2.csv", "1,2,3,4") +
|
||||
endline + "--" + boundary + "--";
|
||||
|
||||
mockRequest.setContent(content.getBytes(StandardCharsets.UTF_8));
|
||||
|
||||
// when
|
||||
multiPart.parse(mockRequest, tempDir);
|
||||
|
||||
// Access private field to verify temporary files are tracked
|
||||
Field tempFilesField = JakartaMultiPartRequest.class.getDeclaredField("temporaryFiles");
|
||||
tempFilesField.setAccessible(true);
|
||||
@SuppressWarnings("unchecked")
|
||||
List<File> temporaryFiles = (List<File>) tempFilesField.get(multiPart);
|
||||
|
||||
// Store file paths before cleanup for verification
|
||||
List<String> tempFilePaths = temporaryFiles.stream()
|
||||
.map(File::getAbsolutePath)
|
||||
.toList();
|
||||
|
||||
// Verify temporary files exist before cleanup
|
||||
assertThat(temporaryFiles).isNotEmpty();
|
||||
for (File tempFile : temporaryFiles) {
|
||||
assertThat(tempFile).exists();
|
||||
}
|
||||
|
||||
// when - cleanup
|
||||
multiPart.cleanUp();
|
||||
|
||||
// then - verify files are deleted and tracking list is cleared
|
||||
for (String tempFilePath : tempFilePaths) {
|
||||
assertThat(new File(tempFilePath)).doesNotExist();
|
||||
}
|
||||
assertThat(temporaryFiles).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void cleanupMethodsCanBeOverridden() {
|
||||
// Create a custom implementation to test extensibility
|
||||
class CustomJakartaMultiPartRequest extends JakartaMultiPartRequest {
|
||||
boolean diskFileItemsCleanedUp = false;
|
||||
boolean temporaryFilesCleanedUp = false;
|
||||
|
||||
@Override
|
||||
protected void cleanUpDiskFileItems() {
|
||||
diskFileItemsCleanedUp = true;
|
||||
super.cleanUpDiskFileItems();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void cleanUpTemporaryFiles() {
|
||||
temporaryFilesCleanedUp = true;
|
||||
super.cleanUpTemporaryFiles();
|
||||
}
|
||||
}
|
||||
|
||||
CustomJakartaMultiPartRequest customMultiPart = new CustomJakartaMultiPartRequest();
|
||||
|
||||
// when
|
||||
customMultiPart.cleanUp();
|
||||
|
||||
// then
|
||||
assertThat(customMultiPart.diskFileItemsCleanedUp).isTrue();
|
||||
assertThat(customMultiPart.temporaryFilesCleanedUp).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void temporaryFileCreationFailureAddsError() throws IOException {
|
||||
// Create a custom implementation that simulates temp file creation failure
|
||||
class FaultyJakartaMultiPartRequest extends JakartaMultiPartRequest {
|
||||
@Override
|
||||
protected void processFileField(DiskFileItem item, String saveDir) {
|
||||
// Simulate in-memory upload that fails to create temp file
|
||||
if (item.isInMemory()) {
|
||||
try {
|
||||
// Simulate IOException during temp file creation
|
||||
throw new IOException("Simulated temp file creation failure");
|
||||
} catch (IOException e) {
|
||||
// Add the error to the errors list for proper user feedback
|
||||
LocalizedMessage errorMessage = buildErrorMessage(e.getClass(), e.getMessage(),
|
||||
new Object[]{item.getName()});
|
||||
if (!errors.contains(errorMessage)) {
|
||||
errors.add(errorMessage);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
super.processFileField(item, saveDir);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
FaultyJakartaMultiPartRequest faultyMultiPart = new FaultyJakartaMultiPartRequest();
|
||||
|
||||
// given - small file that would normally be in-memory
|
||||
String content = formFile("file1", "test1.csv", "a,b") +
|
||||
endline + "--" + boundary + "--";
|
||||
|
||||
mockRequest.setContent(content.getBytes(StandardCharsets.UTF_8));
|
||||
|
||||
// when
|
||||
faultyMultiPart.parse(mockRequest, tempDir);
|
||||
|
||||
// then - verify error is properly captured
|
||||
assertThat(faultyMultiPart.getErrors())
|
||||
.hasSize(1)
|
||||
.first()
|
||||
.extracting(LocalizedMessage::getTextKey)
|
||||
.isEqualTo("struts.messages.upload.error.IOException");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void temporaryFileCreationErrorsAreNotDuplicated() throws IOException {
|
||||
// Test that duplicate errors are not added to the errors list
|
||||
JakartaMultiPartRequest multiPartWithDuplicateErrors = new JakartaMultiPartRequest();
|
||||
|
||||
// Simulate adding the same error twice
|
||||
IOException testException = new IOException("Test exception");
|
||||
LocalizedMessage errorMessage = multiPartWithDuplicateErrors.buildErrorMessage(
|
||||
testException.getClass(), testException.getMessage(), new Object[]{"test.csv"});
|
||||
|
||||
// when - add same error twice
|
||||
multiPartWithDuplicateErrors.errors.add(errorMessage);
|
||||
if (!multiPartWithDuplicateErrors.errors.contains(errorMessage)) {
|
||||
multiPartWithDuplicateErrors.errors.add(errorMessage);
|
||||
}
|
||||
|
||||
// then - only one error should be present
|
||||
assertThat(multiPartWithDuplicateErrors.getErrors()).hasSize(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void cleanupIsIdempotent() throws IOException {
|
||||
// given - process some files
|
||||
String content = formFile("file1", "test1.csv", "1,2,3,4") +
|
||||
endline + "--" + boundary + "--";
|
||||
|
||||
mockRequest.setContent(content.getBytes(StandardCharsets.UTF_8));
|
||||
multiPart.parse(mockRequest, tempDir);
|
||||
|
||||
// when - call cleanup multiple times
|
||||
multiPart.cleanUp();
|
||||
multiPart.cleanUp();
|
||||
multiPart.cleanUp();
|
||||
|
||||
// then - should not throw exceptions and should be safe
|
||||
assertThat(multiPart.uploadedFiles).isEmpty();
|
||||
assertThat(multiPart.parameters).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void endToEndMultipartProcessingWithCleanup() throws IOException {
|
||||
// Test complete multipart processing lifecycle
|
||||
String content = formFile("file1", "test1.csv", "1,2,3,4") +
|
||||
formField("param1", "value1") +
|
||||
formFile("file2", "test2.csv", "5,6,7,8") +
|
||||
formField("param2", "value2") +
|
||||
endline + "--" + boundary + "--";
|
||||
|
||||
mockRequest.setContent(content.getBytes(StandardCharsets.UTF_8));
|
||||
|
||||
// when - full processing
|
||||
multiPart.parse(mockRequest, tempDir);
|
||||
|
||||
// then - verify everything was processed
|
||||
assertThat(multiPart.getErrors()).isEmpty();
|
||||
assertThat(multiPart.getFile("file1")).hasSize(1);
|
||||
assertThat(multiPart.getFile("file2")).hasSize(1);
|
||||
assertThat(multiPart.getParameter("param1")).isEqualTo("value1");
|
||||
assertThat(multiPart.getParameter("param2")).isEqualTo("value2");
|
||||
|
||||
// when - cleanup
|
||||
multiPart.cleanUp();
|
||||
|
||||
// then - verify complete cleanup
|
||||
assertThat(multiPart.uploadedFiles).isEmpty();
|
||||
assertThat(multiPart.parameters).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void temporaryFilesCreatedInSaveDirectory() throws IOException, NoSuchFieldException, IllegalAccessException {
|
||||
// Test that temporary files for in-memory uploads are created in the saveDir, not system temp
|
||||
String content = formFile("file1", "test1.csv", "small,content") +
|
||||
endline + "--" + boundary + "--";
|
||||
|
||||
mockRequest.setContent(content.getBytes(StandardCharsets.UTF_8));
|
||||
|
||||
// when
|
||||
multiPart.parse(mockRequest, tempDir);
|
||||
|
||||
// Access private field to get temporary files
|
||||
Field tempFilesField = JakartaMultiPartRequest.class.getDeclaredField("temporaryFiles");
|
||||
tempFilesField.setAccessible(true);
|
||||
@SuppressWarnings("unchecked")
|
||||
List<File> temporaryFiles = (List<File>) tempFilesField.get(multiPart);
|
||||
|
||||
// then - verify temporary files are created in saveDir
|
||||
assertThat(temporaryFiles).isNotEmpty();
|
||||
for (File tempFile : temporaryFiles) {
|
||||
// Verify the temporary file is in the saveDir, not system temp
|
||||
assertThat(tempFile.getParent()).isEqualTo(tempDir);
|
||||
assertThat(tempFile.getName()).startsWith("upload_");
|
||||
assertThat(tempFile.getName()).endsWith(".tmp");
|
||||
assertThat(tempFile).exists();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void secureTemporaryFileNaming() throws IOException, NoSuchFieldException, IllegalAccessException {
|
||||
// Test that temporary files use UUID-based naming for security
|
||||
String content = formFile("file1", "malicious../../../etc/passwd", "content") +
|
||||
endline + "--" + boundary + "--";
|
||||
|
||||
mockRequest.setContent(content.getBytes(StandardCharsets.UTF_8));
|
||||
|
||||
// when
|
||||
multiPart.parse(mockRequest, tempDir);
|
||||
|
||||
// Access private field to get temporary files
|
||||
Field tempFilesField = JakartaMultiPartRequest.class.getDeclaredField("temporaryFiles");
|
||||
tempFilesField.setAccessible(true);
|
||||
@SuppressWarnings("unchecked")
|
||||
List<File> temporaryFiles = (List<File>) tempFilesField.get(multiPart);
|
||||
|
||||
// then - verify secure naming prevents directory traversal
|
||||
assertThat(temporaryFiles).isNotEmpty();
|
||||
for (File tempFile : temporaryFiles) {
|
||||
// Verify the temporary file uses secure UUID naming
|
||||
assertThat(tempFile.getName()).startsWith("upload_");
|
||||
assertThat(tempFile.getName()).endsWith(".tmp");
|
||||
// Verify it doesn't contain malicious path elements
|
||||
assertThat(tempFile.getName()).doesNotContain("..");
|
||||
assertThat(tempFile.getName()).doesNotContain("/");
|
||||
assertThat(tempFile.getName()).doesNotContain("\\");
|
||||
// Verify it's in the correct directory
|
||||
assertThat(tempFile.getParent()).isEqualTo(tempDir);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void processNormalFormFieldHandlesNullFieldName() throws IOException {
|
||||
// Test null field name handling in processNormalFormField
|
||||
String content =
|
||||
endline + "--" + boundary + endline +
|
||||
"Content-Disposition: form-data" + endline + // No name attribute
|
||||
endline +
|
||||
"field value without name" +
|
||||
endline + "--" + boundary + endline +
|
||||
"Content-Disposition: form-data; name=\"validfield\"" + endline +
|
||||
endline +
|
||||
"valid field value" +
|
||||
endline + "--" + boundary + "--";
|
||||
|
||||
mockRequest.setContent(content.getBytes(StandardCharsets.UTF_8));
|
||||
|
||||
// when
|
||||
multiPart.parse(mockRequest, tempDir);
|
||||
|
||||
// then - should only process the valid field
|
||||
assertThat(multiPart.getErrors()).isEmpty();
|
||||
assertThat(multiPart.getParameter("validfield")).isEqualTo("valid field value");
|
||||
assertThat(multiPart.getParameterNames().asIterator()).toIterable().hasSize(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void processFileFieldHandlesNullFieldName() throws IOException {
|
||||
// Test null field name handling in processFileField
|
||||
String content =
|
||||
endline + "--" + boundary + endline +
|
||||
"Content-Disposition: form-data; filename=\"orphan.txt\"" + endline + // No name attribute
|
||||
"Content-Type: text/plain" + endline +
|
||||
endline +
|
||||
"orphaned file content" +
|
||||
endline + "--" + boundary + endline +
|
||||
"Content-Disposition: form-data; name=\"validfile\"; filename=\"valid.txt\"" + endline +
|
||||
"Content-Type: text/plain" + endline +
|
||||
endline +
|
||||
"valid file content" +
|
||||
endline + "--" + boundary + "--";
|
||||
|
||||
mockRequest.setContent(content.getBytes(StandardCharsets.UTF_8));
|
||||
|
||||
// when
|
||||
multiPart.parse(mockRequest, tempDir);
|
||||
|
||||
// then - should only process the valid file
|
||||
assertThat(multiPart.getErrors()).isEmpty();
|
||||
assertThat(multiPart.uploadedFiles).hasSize(1);
|
||||
assertThat(multiPart.getFile("validfile")).hasSize(1);
|
||||
assertThat(multiPart.getFile("validfile")[0].getContent())
|
||||
.asInstanceOf(InstanceOfAssertFactories.FILE)
|
||||
.content()
|
||||
.isEqualTo("valid file content");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void diskFileItemCleanupCoverage() throws IOException, NoSuchFieldException, IllegalAccessException {
|
||||
// Test disk file item cleanup paths
|
||||
String content = formFile("file1", "test1.csv", "1,2,3,4") +
|
||||
endline + "--" + boundary + "--";
|
||||
|
||||
mockRequest.setContent(content.getBytes(StandardCharsets.UTF_8));
|
||||
|
||||
// when - force files to disk with small buffer
|
||||
multiPart.setBufferSize("1");
|
||||
multiPart.parse(mockRequest, tempDir);
|
||||
|
||||
// Access private field to verify disk file items are tracked
|
||||
Field diskFileItemsField = JakartaMultiPartRequest.class.getDeclaredField("diskFileItems");
|
||||
diskFileItemsField.setAccessible(true);
|
||||
@SuppressWarnings("unchecked")
|
||||
java.util.List<org.apache.commons.fileupload2.core.DiskFileItem> diskFileItems =
|
||||
(java.util.List<org.apache.commons.fileupload2.core.DiskFileItem>) diskFileItemsField.get(multiPart);
|
||||
|
||||
// then - should have disk file items tracked
|
||||
assertThat(diskFileItems).isNotEmpty();
|
||||
|
||||
// when - cleanup
|
||||
multiPart.cleanUp();
|
||||
|
||||
// then - should clear tracking
|
||||
assertThat(diskFileItems).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void inMemoryVsDiskFileHandling() throws IOException {
|
||||
// Test both in-memory and disk file handling paths
|
||||
String smallContent = "small"; // Should be in-memory
|
||||
String largeContent = "x".repeat(20000); // Should go to disk
|
||||
|
||||
String content = formFile("smallfile", "small.txt", smallContent) +
|
||||
formFile("largefile", "large.txt", largeContent) +
|
||||
endline + "--" + boundary + "--";
|
||||
|
||||
mockRequest.setContent(content.getBytes(StandardCharsets.UTF_8));
|
||||
|
||||
// when - use default buffer size
|
||||
multiPart.parse(mockRequest, tempDir);
|
||||
|
||||
// then - both files should be processed
|
||||
assertThat(multiPart.getErrors()).isEmpty();
|
||||
assertThat(multiPart.uploadedFiles).hasSize(2);
|
||||
assertThat(multiPart.getFile("smallfile")).hasSize(1);
|
||||
assertThat(multiPart.getFile("largefile")).hasSize(1);
|
||||
|
||||
// Verify content
|
||||
assertThat(multiPart.getFile("smallfile")[0].getContent())
|
||||
.asInstanceOf(InstanceOfAssertFactories.FILE)
|
||||
.content()
|
||||
.isEqualTo(smallContent);
|
||||
assertThat(multiPart.getFile("largefile")[0].getContent())
|
||||
.asInstanceOf(InstanceOfAssertFactories.FILE)
|
||||
.content()
|
||||
.isEqualTo(largeContent);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void errorDuplicationPrevention() throws IOException {
|
||||
// Test that duplicate errors are not added
|
||||
JakartaMultiPartRequest multiPartRequest = new JakartaMultiPartRequest();
|
||||
|
||||
// Simulate adding the same error multiple times
|
||||
IOException testException = new IOException("Test error");
|
||||
LocalizedMessage errorMessage = multiPartRequest.buildErrorMessage(
|
||||
testException.getClass(), testException.getMessage(), new Object[]{"test.csv"});
|
||||
|
||||
// when - try to add same error multiple times
|
||||
multiPartRequest.errors.add(errorMessage);
|
||||
if (!multiPartRequest.errors.contains(errorMessage)) {
|
||||
multiPartRequest.errors.add(errorMessage); // Should not be added
|
||||
}
|
||||
if (!multiPartRequest.errors.contains(errorMessage)) {
|
||||
multiPartRequest.errors.add(errorMessage); // Should not be added
|
||||
}
|
||||
|
||||
// then - should only have one error
|
||||
assertThat(multiPartRequest.getErrors()).hasSize(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void processFileFieldHandlesEmptyFileName() throws IOException {
|
||||
String content =
|
||||
endline + "--" + boundary + endline +
|
||||
"Content-Disposition: form-data; name=\"emptyfile\"; filename=\"\"" + endline +
|
||||
"Content-Type: text/plain" + endline +
|
||||
endline +
|
||||
"some content that should be ignored" +
|
||||
endline + "--" + boundary + endline +
|
||||
"Content-Disposition: form-data; name=\"validfile\"; filename=\"test.txt\"" + endline +
|
||||
"Content-Type: text/plain" + endline +
|
||||
endline +
|
||||
"valid file content" +
|
||||
endline + "--" + boundary + "--";
|
||||
|
||||
mockRequest.setContent(content.getBytes(StandardCharsets.UTF_8));
|
||||
|
||||
// when
|
||||
multiPart.parse(mockRequest, tempDir);
|
||||
|
||||
// then - should only process the file with valid filename
|
||||
assertThat(multiPart.getErrors()).isEmpty();
|
||||
assertThat(multiPart.uploadedFiles).hasSize(1);
|
||||
assertThat(multiPart.getFile("validfile")).hasSize(1);
|
||||
assertThat(multiPart.getFile("emptyfile")).isEmpty();
|
||||
assertThat(multiPart.getFile("validfile")[0].getContent())
|
||||
.asInstanceOf(InstanceOfAssertFactories.FILE)
|
||||
.content()
|
||||
.isEqualTo("valid file content");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+320
@@ -23,10 +23,18 @@ import org.apache.struts2.dispatcher.LocalizedMessage;
|
||||
import org.assertj.core.api.InstanceOfAssertFactories;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.lang.reflect.Method;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
|
||||
public class JakartaStreamMultiPartRequestTest extends AbstractMultiPartRequestTest {
|
||||
|
||||
@@ -71,4 +79,316 @@ public class JakartaStreamMultiPartRequestTest extends AbstractMultiPartRequestT
|
||||
.containsExactly("struts.messages.upload.error.FileUploadSizeException");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void readStreamProperlyHandlesResources() throws Exception {
|
||||
// Create a test input stream with known data
|
||||
byte[] testData = "test data for stream reading".getBytes(StandardCharsets.UTF_8);
|
||||
InputStream testStream = new java.io.ByteArrayInputStream(testData);
|
||||
|
||||
JakartaStreamMultiPartRequest streamMultiPart = new JakartaStreamMultiPartRequest();
|
||||
|
||||
// Use reflection to access private readStream method
|
||||
Method readStreamMethod = JakartaStreamMultiPartRequest.class.getDeclaredMethod("readStream", InputStream.class);
|
||||
readStreamMethod.setAccessible(true);
|
||||
|
||||
// when
|
||||
String result = (String) readStreamMethod.invoke(streamMultiPart, testStream);
|
||||
|
||||
// then
|
||||
assertThat(result).isEqualTo("test data for stream reading");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void readStreamHandlesExceptionsProperly() throws Exception {
|
||||
// Create a stream that throws an exception
|
||||
InputStream faultyStream = new InputStream() {
|
||||
@Override
|
||||
public int read() throws IOException {
|
||||
throw new IOException("Simulated stream failure");
|
||||
}
|
||||
};
|
||||
|
||||
JakartaStreamMultiPartRequest streamMultiPart = new JakartaStreamMultiPartRequest();
|
||||
|
||||
// Use reflection to access private readStream method
|
||||
Method readStreamMethod = JakartaStreamMultiPartRequest.class.getDeclaredMethod("readStream", InputStream.class);
|
||||
readStreamMethod.setAccessible(true);
|
||||
|
||||
// when/then - should propagate the exception
|
||||
assertThatThrownBy(() -> readStreamMethod.invoke(streamMultiPart, faultyStream))
|
||||
.isInstanceOf(InvocationTargetException.class)
|
||||
.cause()
|
||||
.isInstanceOf(IOException.class)
|
||||
.hasMessage("Simulated stream failure");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void readStreamHandlesEmptyStream() throws Exception {
|
||||
// Create an empty stream
|
||||
InputStream emptyStream = new java.io.ByteArrayInputStream(new byte[0]);
|
||||
|
||||
JakartaStreamMultiPartRequest streamMultiPart = new JakartaStreamMultiPartRequest();
|
||||
|
||||
// Use reflection to access private readStream method
|
||||
Method readStreamMethod = JakartaStreamMultiPartRequest.class.getDeclaredMethod("readStream", InputStream.class);
|
||||
readStreamMethod.setAccessible(true);
|
||||
|
||||
// when
|
||||
String result = (String) readStreamMethod.invoke(streamMultiPart, emptyStream);
|
||||
|
||||
// then
|
||||
assertThat(result).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void readStreamHandlesLargeData() throws Exception {
|
||||
// Create a large data stream to test buffer handling
|
||||
StringBuilder largeData = new StringBuilder();
|
||||
for (int i = 0; i < 2000; i++) {
|
||||
largeData.append("line").append(i).append("\n");
|
||||
}
|
||||
|
||||
byte[] testData = largeData.toString().getBytes(StandardCharsets.UTF_8);
|
||||
InputStream largeStream = new java.io.ByteArrayInputStream(testData);
|
||||
|
||||
JakartaStreamMultiPartRequest streamMultiPart = new JakartaStreamMultiPartRequest();
|
||||
|
||||
// Use reflection to access private readStream method
|
||||
Method readStreamMethod = JakartaStreamMultiPartRequest.class.getDeclaredMethod("readStream", InputStream.class);
|
||||
readStreamMethod.setAccessible(true);
|
||||
|
||||
// when
|
||||
String result = (String) readStreamMethod.invoke(streamMultiPart, largeStream);
|
||||
|
||||
// then
|
||||
assertThat(result).isEqualTo(largeData.toString());
|
||||
assertThat(result.length()).isGreaterThan(1024); // Verify it's larger than internal buffer
|
||||
}
|
||||
|
||||
@Test
|
||||
public void processFileItemAsFormFieldHandlesNullFieldName() throws IOException {
|
||||
// Test the null field name path in processFileItemAsFormField
|
||||
String content = formFile("", "test.csv", "data") + // Field name will be empty/null-like
|
||||
endline + "--" + boundary + "--";
|
||||
|
||||
mockRequest.setContent(content.getBytes(StandardCharsets.UTF_8));
|
||||
|
||||
// when
|
||||
multiPart.parse(mockRequest, tempDir);
|
||||
|
||||
// then - should complete without error, but no parameters should be added
|
||||
assertThat(multiPart.getErrors()).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void processFileItemAsFileFieldHandlesNullFieldName() throws IOException {
|
||||
// This test covers the null field name path in processFileItemAsFileField
|
||||
JakartaStreamMultiPartRequest streamMultiPart = new JakartaStreamMultiPartRequest();
|
||||
|
||||
// Create a mock file item with null field name
|
||||
String content = "--" + boundary + endline +
|
||||
"Content-Disposition: form-data; filename=\"test.csv\"" + endline +
|
||||
"Content-Type: text/csv" + endline +
|
||||
endline +
|
||||
"test data" +
|
||||
endline + "--" + boundary + "--";
|
||||
|
||||
mockRequest.setContent(content.getBytes(StandardCharsets.UTF_8));
|
||||
|
||||
// when
|
||||
streamMultiPart.parse(mockRequest, tempDir);
|
||||
|
||||
// then - should complete without error but no files should be uploaded
|
||||
assertThat(streamMultiPart.getErrors()).isEmpty();
|
||||
assertThat(streamMultiPart.uploadedFiles).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void exceedsMaxFilesPath() throws IOException {
|
||||
// Test the exceedsMaxFiles method path
|
||||
String content = formFile("file1", "test1.csv", "data1") +
|
||||
formFile("file2", "test2.csv", "data2") +
|
||||
formFile("file3", "test3.csv", "data3") +
|
||||
endline + "--" + boundary + "--";
|
||||
|
||||
mockRequest.setContent(content.getBytes(StandardCharsets.UTF_8));
|
||||
|
||||
// when - set max files to 1
|
||||
multiPart.setMaxFiles("1");
|
||||
multiPart.parse(mockRequest, tempDir);
|
||||
|
||||
// then - should have only 1 file and errors for others
|
||||
assertThat(multiPart.uploadedFiles).hasSize(1);
|
||||
assertThat(multiPart.getErrors())
|
||||
.isNotEmpty()
|
||||
.allSatisfy(error ->
|
||||
assertThat(error.getTextKey()).isEqualTo("struts.messages.upload.error.FileUploadFileCountLimitException")
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void actualSizeOfUploadedFilesCalculation() throws IOException {
|
||||
// Test the actualSizeOfUploadedFiles method
|
||||
String content = formFile("file1", "test1.csv", "data1234567890") + // 14 bytes + headers
|
||||
formFile("file2", "test2.csv", "moredata") + // 8 bytes + headers
|
||||
endline + "--" + boundary + "--";
|
||||
|
||||
mockRequest.setContent(content.getBytes(StandardCharsets.UTF_8));
|
||||
|
||||
// when
|
||||
multiPart.parse(mockRequest, tempDir);
|
||||
|
||||
// then - should have uploaded files and calculate their total size
|
||||
assertThat(multiPart.uploadedFiles).hasSize(2);
|
||||
assertThat(multiPart.getFile("file1")).hasSize(1);
|
||||
assertThat(multiPart.getFile("file2")).hasSize(1);
|
||||
|
||||
// Verify files have the expected content
|
||||
assertThat(multiPart.getFile("file1")[0].getContent())
|
||||
.asInstanceOf(InstanceOfAssertFactories.FILE)
|
||||
.content()
|
||||
.isEqualTo("data1234567890");
|
||||
assertThat(multiPart.getFile("file2")[0].getContent())
|
||||
.asInstanceOf(InstanceOfAssertFactories.FILE)
|
||||
.content()
|
||||
.isEqualTo("moredata");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void createTemporaryFileMethod() throws Exception {
|
||||
// Test the createTemporaryFile method directly
|
||||
JakartaStreamMultiPartRequest streamMultiPart = new JakartaStreamMultiPartRequest();
|
||||
Path testLocation = Paths.get(tempDir);
|
||||
|
||||
// when
|
||||
File tempFile1 = streamMultiPart.createTemporaryFile("test.csv", testLocation);
|
||||
File tempFile2 = streamMultiPart.createTemporaryFile("another.txt", testLocation);
|
||||
|
||||
// then
|
||||
assertThat(tempFile1.getName()).startsWith("upload_");
|
||||
assertThat(tempFile1.getName()).endsWith(".tmp");
|
||||
assertThat(tempFile1.getParent()).isEqualTo(tempDir);
|
||||
|
||||
assertThat(tempFile2.getName()).startsWith("upload_");
|
||||
assertThat(tempFile2.getName()).endsWith(".tmp");
|
||||
assertThat(tempFile2.getParent()).isEqualTo(tempDir);
|
||||
|
||||
// Should be unique names
|
||||
assertThat(tempFile1.getName()).isNotEqualTo(tempFile2.getName());
|
||||
|
||||
// Clean up
|
||||
tempFile1.delete();
|
||||
tempFile2.delete();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void streamFileToDiskWithDifferentBufferSizes() throws IOException {
|
||||
// Test streamFileToDisk with different buffer sizes
|
||||
String largeContent = "x".repeat(5000); // Content larger than default buffer
|
||||
String content = formFile("largefile", "large.csv", largeContent) +
|
||||
endline + "--" + boundary + "--";
|
||||
|
||||
mockRequest.setContent(content.getBytes(StandardCharsets.UTF_8));
|
||||
|
||||
// when - use small buffer size to ensure multiple reads
|
||||
multiPart.setBufferSize("100");
|
||||
multiPart.parse(mockRequest, tempDir);
|
||||
|
||||
// then
|
||||
assertThat(multiPart.getErrors()).isEmpty();
|
||||
assertThat(multiPart.getFile("largefile")).hasSize(1);
|
||||
assertThat(multiPart.getFile("largefile")[0].getContent())
|
||||
.asInstanceOf(InstanceOfAssertFactories.FILE)
|
||||
.content()
|
||||
.isEqualTo(largeContent);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void exceedsMaxSizeOfFilesWithFileCleanup() throws IOException {
|
||||
// Test the file deletion path when max size is exceeded
|
||||
String content = formFile("file1", "test1.csv", "small") +
|
||||
formFile("file2", "test2.csv", "this is a much larger file content that should exceed the limit") +
|
||||
endline + "--" + boundary + "--";
|
||||
|
||||
mockRequest.setContent(content.getBytes(StandardCharsets.UTF_8));
|
||||
|
||||
// when - set very small max size
|
||||
multiPart.setMaxSizeOfFiles("20");
|
||||
multiPart.parse(mockRequest, tempDir);
|
||||
|
||||
// then - should have first file uploaded but error for second
|
||||
assertThat(multiPart.uploadedFiles).hasSize(1);
|
||||
assertThat(multiPart.getFile("file1")).hasSize(1);
|
||||
assertThat(multiPart.getFile("file2")).isEmpty();
|
||||
assertThat(multiPart.getErrors())
|
||||
.isNotEmpty()
|
||||
.anyMatch(error ->
|
||||
error.getTextKey().equals("struts.messages.upload.error.FileUploadSizeException")
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void createUploadedFileWithVariousContentTypes() throws IOException {
|
||||
// Test different content types and file names
|
||||
String content =
|
||||
endline + "--" + boundary + endline +
|
||||
"Content-Disposition: form-data; name=\"textfile\"; filename=\"document.txt\"" + endline +
|
||||
"Content-Type: text/plain" + endline +
|
||||
endline +
|
||||
"Plain text content" +
|
||||
endline + "--" + boundary + endline +
|
||||
"Content-Disposition: form-data; name=\"jsonfile\"; filename=\"data.json\"" + endline +
|
||||
"Content-Type: application/json" + endline +
|
||||
endline +
|
||||
"{\"key\": \"value\"}" +
|
||||
endline + "--" + boundary + "--";
|
||||
|
||||
mockRequest.setContent(content.getBytes(StandardCharsets.UTF_8));
|
||||
|
||||
// when
|
||||
multiPart.parse(mockRequest, tempDir);
|
||||
|
||||
// then
|
||||
assertThat(multiPart.getErrors()).isEmpty();
|
||||
assertThat(multiPart.uploadedFiles).hasSize(2);
|
||||
|
||||
// Verify text file
|
||||
assertThat(multiPart.getFile("textfile")).hasSize(1);
|
||||
assertThat(multiPart.getFile("textfile")[0].getContentType()).isEqualTo("text/plain");
|
||||
assertThat(multiPart.getFile("textfile")[0].getOriginalName()).isEqualTo("document.txt");
|
||||
|
||||
// Verify JSON file
|
||||
assertThat(multiPart.getFile("jsonfile")).hasSize(1);
|
||||
assertThat(multiPart.getFile("jsonfile")[0].getContentType()).isEqualTo("application/json");
|
||||
assertThat(multiPart.getFile("jsonfile")[0].getOriginalName()).isEqualTo("data.json");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void emptyFileNameFieldsAreSkipped() throws IOException {
|
||||
// Test files with empty names are skipped
|
||||
String content =
|
||||
endline + "--" + boundary + endline +
|
||||
"Content-Disposition: form-data; name=\"emptyfile\"; filename=\"\"" + endline +
|
||||
"Content-Type: text/plain" + endline +
|
||||
endline +
|
||||
"This should be skipped" +
|
||||
endline + "--" + boundary + endline +
|
||||
"Content-Disposition: form-data; name=\"validfile\"; filename=\"valid.txt\"" + endline +
|
||||
"Content-Type: text/plain" + endline +
|
||||
endline +
|
||||
"This should be processed" +
|
||||
endline + "--" + boundary + "--";
|
||||
|
||||
mockRequest.setContent(content.getBytes(StandardCharsets.UTF_8));
|
||||
|
||||
// when
|
||||
multiPart.parse(mockRequest, tempDir);
|
||||
|
||||
// then
|
||||
assertThat(multiPart.getErrors()).isEmpty();
|
||||
assertThat(multiPart.uploadedFiles).hasSize(1);
|
||||
assertThat(multiPart.getFile("emptyfile")).isEmpty();
|
||||
assertThat(multiPart.getFile("validfile")).hasSize(1);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+201
-199
@@ -18,21 +18,21 @@
|
||||
*/
|
||||
package org.apache.struts2.interceptor;
|
||||
|
||||
import org.apache.struts2.ActionContext;
|
||||
import org.apache.struts2.ActionSupport;
|
||||
import org.apache.struts2.locale.DefaultLocaleProvider;
|
||||
import org.apache.struts2.ValidationAwareSupport;
|
||||
import org.apache.struts2.mock.MockActionInvocation;
|
||||
import org.apache.struts2.mock.MockActionProxy;
|
||||
import org.apache.struts2.util.ClassLoaderUtil;
|
||||
import org.apache.commons.fileupload2.jakarta.servlet6.JakartaServletDiskFileUpload;
|
||||
import org.apache.commons.fileupload2.jakarta.servlet6.JakartaServletFileUpload;
|
||||
import org.apache.struts2.ActionContext;
|
||||
import org.apache.struts2.ActionSupport;
|
||||
import org.apache.struts2.StrutsInternalTestCase;
|
||||
import org.apache.struts2.ValidationAwareSupport;
|
||||
import org.apache.struts2.action.UploadedFilesAware;
|
||||
import org.apache.struts2.dispatcher.multipart.JakartaMultiPartRequest;
|
||||
import org.apache.struts2.dispatcher.multipart.MultiPartRequestWrapper;
|
||||
import org.apache.struts2.dispatcher.multipart.StrutsUploadedFile;
|
||||
import org.apache.struts2.dispatcher.multipart.UploadedFile;
|
||||
import org.apache.struts2.locale.DefaultLocaleProvider;
|
||||
import org.apache.struts2.mock.MockActionInvocation;
|
||||
import org.apache.struts2.mock.MockActionProxy;
|
||||
import org.apache.struts2.util.ClassLoaderUtil;
|
||||
import org.assertj.core.util.Files;
|
||||
import org.springframework.mock.web.MockHttpServletRequest;
|
||||
|
||||
@@ -40,7 +40,6 @@ import java.io.File;
|
||||
import java.net.URI;
|
||||
import java.net.URL;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
@@ -51,53 +50,6 @@ import static org.assertj.core.api.Assertions.assertThat;
|
||||
*/
|
||||
public class ActionFileUploadInterceptorTest extends StrutsInternalTestCase {
|
||||
|
||||
private static final UploadedFile EMPTY_FILE = new UploadedFile() {
|
||||
@Override
|
||||
public Long length() {
|
||||
return 0L;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "";
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isFile() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean delete() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getAbsolutePath() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public File getContent() {
|
||||
return Files.newTemporaryFile();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getOriginalName() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getContentType() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getInputName() {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
private MockHttpServletRequest request;
|
||||
private ActionFileUploadInterceptor interceptor;
|
||||
private File tempDir;
|
||||
@@ -105,16 +57,16 @@ public class ActionFileUploadInterceptorTest extends StrutsInternalTestCase {
|
||||
private final String htmlContent = "<html><head></head><body>html content</body></html>";
|
||||
private final String plainContent = "plain content";
|
||||
private final String boundary = "simple boundary";
|
||||
private final String endline = "\r\n";
|
||||
private final String endLine = "\r\n";
|
||||
|
||||
public void testAcceptFileWithEmptyAllowedTypesAndExtensions() {
|
||||
// when allowed type is empty
|
||||
ValidationAwareSupport validation = new ValidationAwareSupport();
|
||||
boolean ok = interceptor.acceptFile(validation, EMPTY_FILE, "filename", "text/plain", "inputName");
|
||||
boolean ok = interceptor.acceptFile(validation, createTestFile(Files.newTemporaryFile()), "filename", "text/plain", "inputName");
|
||||
|
||||
assertTrue(ok);
|
||||
assertTrue(validation.getFieldErrors().isEmpty());
|
||||
assertFalse(validation.hasErrors());
|
||||
assertThat(ok).isTrue();
|
||||
assertThat(validation.getFieldErrors()).isEmpty();
|
||||
assertThat(validation.hasErrors()).isFalse();
|
||||
}
|
||||
|
||||
public void testAcceptFileWithoutEmptyTypes() {
|
||||
@@ -122,39 +74,38 @@ public class ActionFileUploadInterceptorTest extends StrutsInternalTestCase {
|
||||
|
||||
// when file is of allowed types
|
||||
ValidationAwareSupport validation = new ValidationAwareSupport();
|
||||
boolean ok = interceptor.acceptFile(validation, EMPTY_FILE, "filename.txt", "text/plain", "inputName");
|
||||
boolean ok = interceptor.acceptFile(validation, createTestFile(Files.newTemporaryFile()), "filename.txt", "text/plain", "inputName");
|
||||
|
||||
assertTrue(ok);
|
||||
assertTrue(validation.getFieldErrors().isEmpty());
|
||||
assertFalse(validation.hasErrors());
|
||||
assertThat(ok).isTrue();
|
||||
assertThat(validation.getFieldErrors()).isEmpty();
|
||||
assertThat(validation.hasErrors()).isFalse();
|
||||
|
||||
// when file is not of allowed types
|
||||
validation = new ValidationAwareSupport();
|
||||
boolean notOk = interceptor.acceptFile(validation, EMPTY_FILE, "filename.html", "text/html", "inputName");
|
||||
boolean notOk = interceptor.acceptFile(validation, createTestFile(Files.newTemporaryFile()), "filename.html", "text/html", "inputName");
|
||||
|
||||
assertFalse(notOk);
|
||||
assertFalse(validation.getFieldErrors().isEmpty());
|
||||
assertTrue(validation.hasErrors());
|
||||
assertThat(notOk).isFalse();
|
||||
assertThat(validation.getFieldErrors()).isNotEmpty();
|
||||
assertThat(validation.hasErrors()).isTrue();
|
||||
}
|
||||
|
||||
|
||||
public void testAcceptFileWithWildcardContent() {
|
||||
interceptor.setAllowedTypes("text/*");
|
||||
|
||||
ValidationAwareSupport validation = new ValidationAwareSupport();
|
||||
boolean ok = interceptor.acceptFile(validation, EMPTY_FILE, "filename.txt", "text/plain", "inputName");
|
||||
boolean ok = interceptor.acceptFile(validation, createTestFile(Files.newTemporaryFile()), "filename.txt", "text/plain", "inputName");
|
||||
|
||||
assertTrue(ok);
|
||||
assertTrue(validation.getFieldErrors().isEmpty());
|
||||
assertFalse(validation.hasErrors());
|
||||
assertThat(ok).isTrue();
|
||||
assertThat(validation.getFieldErrors()).isEmpty();
|
||||
assertThat(validation.hasErrors()).isFalse();
|
||||
|
||||
interceptor.setAllowedTypes("text/h*");
|
||||
validation = new ValidationAwareSupport();
|
||||
boolean notOk = interceptor.acceptFile(validation, EMPTY_FILE, "filename.html", "text/plain", "inputName");
|
||||
boolean notOk = interceptor.acceptFile(validation, createTestFile(Files.newTemporaryFile()), "filename.html", "text/plain", "inputName");
|
||||
|
||||
assertFalse(notOk);
|
||||
assertFalse(validation.getFieldErrors().isEmpty());
|
||||
assertTrue(validation.hasErrors());
|
||||
assertThat(notOk).isFalse();
|
||||
assertThat(validation.getFieldErrors()).isNotEmpty();
|
||||
assertThat(validation.hasErrors()).isTrue();
|
||||
}
|
||||
|
||||
public void testAcceptFileWithoutEmptyExtensions() {
|
||||
@@ -162,48 +113,62 @@ public class ActionFileUploadInterceptorTest extends StrutsInternalTestCase {
|
||||
|
||||
// when file is of allowed extensions
|
||||
ValidationAwareSupport validation = new ValidationAwareSupport();
|
||||
boolean ok = interceptor.acceptFile(validation, EMPTY_FILE, "filename.txt", "text/plain", "inputName");
|
||||
boolean ok = interceptor.acceptFile(validation, createTestFile(Files.newTemporaryFile()), "filename.txt", "text/plain", "inputName");
|
||||
|
||||
assertTrue(ok);
|
||||
assertTrue(validation.getFieldErrors().isEmpty());
|
||||
assertFalse(validation.hasErrors());
|
||||
assertThat(ok).isTrue();
|
||||
assertThat(validation.getFieldErrors()).isEmpty();
|
||||
assertThat(validation.hasErrors()).isFalse();
|
||||
|
||||
// when file is not of allowed extensions
|
||||
validation = new ValidationAwareSupport();
|
||||
boolean notOk = interceptor.acceptFile(validation, EMPTY_FILE, "filename.html", "text/html", "inputName");
|
||||
boolean notOk = interceptor.acceptFile(validation, createTestFile(Files.newTemporaryFile()), "filename.html", "text/html", "inputName");
|
||||
|
||||
assertFalse(notOk);
|
||||
assertFalse(validation.getFieldErrors().isEmpty());
|
||||
assertTrue(validation.hasErrors());
|
||||
assertThat(notOk).isFalse();
|
||||
assertThat(validation.getFieldErrors()).isNotEmpty();
|
||||
assertThat(validation.hasErrors()).isTrue();
|
||||
|
||||
//test with multiple extensions
|
||||
interceptor.setAllowedExtensions(".txt,.lol");
|
||||
validation = new ValidationAwareSupport();
|
||||
ok = interceptor.acceptFile(validation, EMPTY_FILE, "filename.lol", "text/plain", "inputName");
|
||||
ok = interceptor.acceptFile(validation, createTestFile(Files.newTemporaryFile()), "filename.lol", "text/plain", "inputName");
|
||||
|
||||
assertTrue(ok);
|
||||
assertTrue(validation.getFieldErrors().isEmpty());
|
||||
assertFalse(validation.hasErrors());
|
||||
assertThat(ok).isTrue();
|
||||
assertThat(validation.getFieldErrors()).isEmpty();
|
||||
assertThat(validation.hasErrors()).isFalse();
|
||||
}
|
||||
|
||||
public void testAcceptFileWithNoFile() {
|
||||
ActionFileUploadInterceptor interceptor = new ActionFileUploadInterceptor();
|
||||
interceptor.setContainer(container);
|
||||
|
||||
interceptor.setAllowedTypes("text/plain");
|
||||
|
||||
// when file is not of allowed types
|
||||
ValidationAwareSupport validation = new ValidationAwareSupport();
|
||||
boolean notOk = interceptor.acceptFile(validation, null, "filename.html", "text/html", "inputName");
|
||||
|
||||
assertFalse(notOk);
|
||||
assertFalse(validation.getFieldErrors().isEmpty());
|
||||
assertTrue(validation.hasErrors());
|
||||
List<String> errors = validation.getFieldErrors().get("inputName");
|
||||
assertEquals(1, errors.size());
|
||||
String msg = errors.get(0);
|
||||
assertTrue(msg.startsWith("Error uploading:"));
|
||||
assertTrue(msg.indexOf("inputName") > 0);
|
||||
assertThat(notOk).isFalse();
|
||||
assertThat(validation.getFieldErrors()).isNotEmpty();
|
||||
assertThat(validation.hasErrors()).isTrue();
|
||||
assertThat(validation.getFieldErrors().get("inputName"))
|
||||
.hasSize(1)
|
||||
.first()
|
||||
.asString()
|
||||
.startsWith("Error uploading:")
|
||||
.contains("inputName");
|
||||
}
|
||||
|
||||
public void testAcceptFileWithNoContent() {
|
||||
interceptor.setAllowedTypes("text/plain");
|
||||
|
||||
ValidationAwareSupport validation = new ValidationAwareSupport();
|
||||
boolean notOk = interceptor.acceptFile(validation, createTestFile(null), "filename.html", "text/plain", "inputName");
|
||||
|
||||
assertThat(notOk).isFalse();
|
||||
assertThat(validation.getFieldErrors()).isNotEmpty();
|
||||
assertThat(validation.hasErrors()).isTrue();
|
||||
assertThat(validation.getFieldErrors().get("inputName"))
|
||||
.hasSize(1)
|
||||
.first()
|
||||
.asString()
|
||||
.startsWith("Error uploading:")
|
||||
.contains("inputName");
|
||||
}
|
||||
|
||||
public void testAcceptFileWithMaxSize() throws Exception {
|
||||
@@ -214,23 +179,18 @@ public class ActionFileUploadInterceptorTest extends StrutsInternalTestCase {
|
||||
|
||||
URL url = ClassLoaderUtil.getResource("log4j2.xml", ActionFileUploadInterceptorTest.class);
|
||||
File file = new File(new URI(url.toString()));
|
||||
assertTrue("log4j2.xml should be in src/test folder", file.exists());
|
||||
assertThat(file).exists();
|
||||
UploadedFile uploadedFile = StrutsUploadedFile.Builder.create(file).withContentType("text/html").withOriginalName("filename").build();
|
||||
boolean notOk = interceptor.acceptFile(validation, uploadedFile, "filename", "text/html", "inputName");
|
||||
|
||||
assertFalse(notOk);
|
||||
assertFalse(validation.getFieldErrors().isEmpty());
|
||||
assertTrue(validation.hasErrors());
|
||||
List<String> errors = validation.getFieldErrors().get("inputName");
|
||||
assertEquals(1, errors.size());
|
||||
String msg = errors.get(0);
|
||||
// the error message should contain at least this test
|
||||
assertThat(msg).contains(
|
||||
"The file is too large to be uploaded",
|
||||
"inputName",
|
||||
"log4j2.xml",
|
||||
"allowed mx size is 10"
|
||||
);
|
||||
assertThat(notOk).isFalse();
|
||||
assertThat(validation.getFieldErrors()).isNotEmpty();
|
||||
assertThat(validation.hasErrors()).isTrue();
|
||||
assertThat(validation.getFieldErrors().get("inputName"))
|
||||
.hasSize(1)
|
||||
.first()
|
||||
.asString()
|
||||
.contains("The file is too large to be uploaded", "inputName", "log4j2.xml", "allowed mx size is 10");
|
||||
}
|
||||
|
||||
public void testNoMultipartRequest() throws Exception {
|
||||
@@ -246,11 +206,11 @@ public class ActionFileUploadInterceptorTest extends StrutsInternalTestCase {
|
||||
mai.setInvocationContext(ActionContext.getContext());
|
||||
|
||||
// if no multipart request it will bypass and execute it
|
||||
assertEquals("NoMultipart", interceptor.intercept(mai));
|
||||
assertThat(interceptor.intercept(mai)).isEqualTo("NoMultipart");
|
||||
}
|
||||
|
||||
public void testInvalidContentTypeMultipartRequest() throws Exception {
|
||||
request.setContentType("multipart/form-data"); // not a multipart contentype
|
||||
request.setContentType("multipart/form-data"); // not a multipart Content-Type
|
||||
request.setMethod("post");
|
||||
|
||||
MyFileUploadAction action = container.inject(MyFileUploadAction.class);
|
||||
@@ -263,7 +223,7 @@ public class ActionFileUploadInterceptorTest extends StrutsInternalTestCase {
|
||||
|
||||
interceptor.intercept(mai);
|
||||
|
||||
assertTrue(action.hasErrors());
|
||||
assertThat(action.hasErrors()).isTrue();
|
||||
}
|
||||
|
||||
public void testNoContentMultipartRequest() throws Exception {
|
||||
@@ -284,7 +244,7 @@ public class ActionFileUploadInterceptorTest extends StrutsInternalTestCase {
|
||||
|
||||
interceptor.intercept(mai);
|
||||
|
||||
assertTrue(action.hasErrors());
|
||||
assertThat(action.hasErrors()).isTrue();
|
||||
}
|
||||
|
||||
public void testSuccessUploadOfATextFileMultipartRequest() throws Exception {
|
||||
@@ -292,7 +252,7 @@ public class ActionFileUploadInterceptorTest extends StrutsInternalTestCase {
|
||||
request.setMethod("post");
|
||||
request.addHeader("Content-type", "multipart/form-data; boundary=---1234");
|
||||
|
||||
// inspired by the unit tests for jakarta commons fileupload
|
||||
// inspired by the unit tests for Jakarta Commons FileUpload
|
||||
String content = ("""
|
||||
-----1234\r
|
||||
Content-Disposition: form-data; name="file"; filename="deleteme.txt"\r
|
||||
@@ -313,14 +273,13 @@ public class ActionFileUploadInterceptorTest extends StrutsInternalTestCase {
|
||||
|
||||
interceptor.intercept(mai);
|
||||
|
||||
assertFalse(action.hasErrors());
|
||||
assertThat(action.hasErrors()).isFalse();
|
||||
|
||||
List<UploadedFile> files = action.getUploadFiles();
|
||||
|
||||
assertNotNull(files);
|
||||
assertEquals(1, files.size());
|
||||
assertEquals("text/html", files.get(0).getContentType());
|
||||
assertNotNull("deleteme.txt", files.get(0).getOriginalName());
|
||||
assertThat(files).isNotNull().hasSize(1);
|
||||
assertThat(files.get(0).getContentType()).isEqualTo("text/html");
|
||||
assertThat(files.get(0).getOriginalName()).isEqualTo("deleteme.txt");
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -334,46 +293,10 @@ public class ActionFileUploadInterceptorTest extends StrutsInternalTestCase {
|
||||
String content = encodeTextFile("test.html", "text/plain", plainContent) +
|
||||
encodeTextFile("test1.html", "text/html", htmlContent) +
|
||||
encodeTextFile("test2.html", "text/html", htmlContent) +
|
||||
endline + "--" + boundary + "--";
|
||||
endLine + "--" + boundary + "--";
|
||||
request.setContent(content.getBytes());
|
||||
|
||||
assertTrue(JakartaServletDiskFileUpload.isMultipartContent(request));
|
||||
|
||||
MyFileUploadAction action = new MyFileUploadAction();
|
||||
container.inject(action);
|
||||
MockActionInvocation mai = new MockActionInvocation();
|
||||
mai.setAction(action);
|
||||
mai.setResultCode("success");
|
||||
mai.setInvocationContext(ActionContext.getContext());
|
||||
ActionContext.getContext().withServletRequest(createMultipartRequestMaxSize(2000));
|
||||
|
||||
interceptor.setAllowedTypes("text/html");
|
||||
interceptor.intercept(mai);
|
||||
|
||||
List<UploadedFile> files = action.getUploadFiles();
|
||||
|
||||
assertNotNull(files);
|
||||
assertEquals("files accepted ", 2, files.size());
|
||||
assertEquals("text/html", files.get(0).getContentType());
|
||||
assertNotNull("test1.html", files.get(0).getOriginalName());
|
||||
}
|
||||
|
||||
public void testUnacceptedNumberOfFiles() throws Exception {
|
||||
request.setCharacterEncoding(StandardCharsets.UTF_8.name());
|
||||
request.setMethod("POST");
|
||||
request.addHeader("Content-type", "multipart/form-data; boundary=" + boundary);
|
||||
String content = encodeTextFile("test.html", "text/plain", plainContent) +
|
||||
encodeTextFile("test1.html", "text/html", htmlContent) +
|
||||
encodeTextFile("test2.html", "text/html", htmlContent) +
|
||||
encodeTextFile("test3.html", "text/html", htmlContent) +
|
||||
endline +
|
||||
"--" +
|
||||
boundary +
|
||||
"--" +
|
||||
endline;
|
||||
request.setContent(content.getBytes());
|
||||
|
||||
assertTrue(JakartaServletFileUpload.isMultipartContent(request));
|
||||
assertThat(JakartaServletDiskFileUpload.isMultipartContent(request)).isTrue();
|
||||
|
||||
MyFileUploadAction action = new MyFileUploadAction();
|
||||
container.inject(action);
|
||||
@@ -386,12 +309,48 @@ public class ActionFileUploadInterceptorTest extends StrutsInternalTestCase {
|
||||
interceptor.setAllowedTypes("text/html");
|
||||
interceptor.intercept(mai);
|
||||
|
||||
assertNull(action.getUploadFiles());
|
||||
assertEquals(1, action.getActionErrors().size());
|
||||
assertEquals(
|
||||
"Request exceeded allowed number of files! Permitted number of files is: 3!",
|
||||
action.getActionErrors().iterator().next()
|
||||
);
|
||||
List<UploadedFile> files = action.getUploadFiles();
|
||||
|
||||
assertThat(files).isNotNull().hasSize(2);
|
||||
assertThat(files.get(0).getContentType()).isEqualTo("text/html");
|
||||
assertThat(files.get(0).getOriginalName()).isEqualTo("test1.html");
|
||||
assertThat(files.get(1).getContentType()).isEqualTo("text/html");
|
||||
assertThat(files.get(1).getOriginalName()).isEqualTo("test2.html");
|
||||
}
|
||||
|
||||
public void testUnacceptedNumberOfFiles() throws Exception {
|
||||
request.setCharacterEncoding(StandardCharsets.UTF_8.name());
|
||||
request.setMethod("POST");
|
||||
request.addHeader("Content-type", "multipart/form-data; boundary=" + boundary);
|
||||
String content = encodeTextFile("test.html", "text/plain", plainContent) +
|
||||
encodeTextFile("test1.html", "text/html", htmlContent) +
|
||||
encodeTextFile("test2.html", "text/html", htmlContent) +
|
||||
encodeTextFile("test3.html", "text/html", htmlContent) +
|
||||
endLine +
|
||||
"--" +
|
||||
boundary +
|
||||
"--" +
|
||||
endLine;
|
||||
request.setContent(content.getBytes());
|
||||
|
||||
assertThat(JakartaServletFileUpload.isMultipartContent(request)).isTrue();
|
||||
|
||||
MyFileUploadAction action = new MyFileUploadAction();
|
||||
container.inject(action);
|
||||
MockActionInvocation mai = new MockActionInvocation();
|
||||
mai.setAction(action);
|
||||
mai.setResultCode("success");
|
||||
mai.setInvocationContext(ActionContext.getContext());
|
||||
ActionContext.getContext().withServletRequest(createMultipartRequestMaxFiles());
|
||||
|
||||
interceptor.setAllowedTypes("text/html");
|
||||
interceptor.intercept(mai);
|
||||
|
||||
assertThat(action.getUploadFiles()).isNull();
|
||||
assertThat(action.getActionErrors())
|
||||
.hasSize(1)
|
||||
.first()
|
||||
.isEqualTo("Request exceeded allowed number of files! Permitted number of files is: 3!");
|
||||
}
|
||||
|
||||
public void testMultipartRequestMaxFileSize() throws Exception {
|
||||
@@ -399,7 +358,7 @@ public class ActionFileUploadInterceptorTest extends StrutsInternalTestCase {
|
||||
request.setMethod("post");
|
||||
request.addHeader("Content-type", "multipart/form-data; boundary=---1234");
|
||||
|
||||
// inspired by the unit tests for jakarta commons fileupload
|
||||
// inspired by the unit tests for Jakarta Commons FileUpload
|
||||
String content = ("""
|
||||
-----1234\r
|
||||
Content-Disposition: form-data; name="file"; filename="deleteme.txt"\r
|
||||
@@ -421,15 +380,12 @@ public class ActionFileUploadInterceptorTest extends StrutsInternalTestCase {
|
||||
|
||||
interceptor.intercept(mai);
|
||||
|
||||
assertTrue(action.hasActionErrors());
|
||||
assertThat(action.hasActionErrors()).isTrue();
|
||||
|
||||
Collection<String> errors = action.getActionErrors();
|
||||
assertEquals(1, errors.size());
|
||||
String msg = errors.iterator().next();
|
||||
// FIXME: the expected size is 40 - length of the string
|
||||
assertEquals(
|
||||
"File deleteme.txt assigned to file exceeded allowed size limit! Max size allowed is: 10 but file was: 11!",
|
||||
msg);
|
||||
assertThat(action.getActionErrors())
|
||||
.hasSize(1)
|
||||
.first()
|
||||
.isEqualTo("File deleteme.txt assigned to file exceeded allowed size limit! Max size allowed is: 10 but file was: 11!");
|
||||
}
|
||||
|
||||
public void testMultipartRequestMaxStringLength() throws Exception {
|
||||
@@ -437,7 +393,7 @@ public class ActionFileUploadInterceptorTest extends StrutsInternalTestCase {
|
||||
request.setMethod("post");
|
||||
request.addHeader("Content-type", "multipart/form-data; boundary=---1234");
|
||||
|
||||
// inspired by the unit tests for jakarta commons fileupload
|
||||
// inspired by the unit tests for Jakarta Commons FileUpload
|
||||
String content = ("""
|
||||
-----1234\r
|
||||
Content-Disposition: form-data; name="file"; filename="deleteme.txt"\r
|
||||
@@ -467,14 +423,12 @@ public class ActionFileUploadInterceptorTest extends StrutsInternalTestCase {
|
||||
|
||||
interceptor.intercept(mai);
|
||||
|
||||
assertTrue(action.hasActionErrors());
|
||||
assertThat(action.hasActionErrors()).isTrue();
|
||||
|
||||
Collection<String> errors = action.getActionErrors();
|
||||
assertEquals(1, errors.size());
|
||||
String msg = errors.iterator().next();
|
||||
assertEquals(
|
||||
"The request parameter \"normalFormField2\" was too long. Max length allowed is 20, but found 27!",
|
||||
msg);
|
||||
assertThat(action.getActionErrors())
|
||||
.hasSize(1)
|
||||
.first()
|
||||
.isEqualTo("The request parameter \"normalFormField2\" was too long. Max length allowed is 20, but found 27!");
|
||||
}
|
||||
|
||||
public void testMultipartRequestLocalizedError() throws Exception {
|
||||
@@ -482,7 +436,6 @@ public class ActionFileUploadInterceptorTest extends StrutsInternalTestCase {
|
||||
request.setMethod("post");
|
||||
request.addHeader("Content-type", "multipart/form-data; boundary=---1234");
|
||||
|
||||
// inspired by the unit tests for jakarta commons fileupload
|
||||
String content = ("""
|
||||
-----1234\r
|
||||
Content-Disposition: form-data; name="file"; filename="deleteme.txt"\r
|
||||
@@ -505,24 +458,24 @@ public class ActionFileUploadInterceptorTest extends StrutsInternalTestCase {
|
||||
|
||||
interceptor.intercept(mai);
|
||||
|
||||
assertTrue(action.hasActionErrors());
|
||||
assertThat(action.hasActionErrors()).isTrue();
|
||||
|
||||
Collection<String> errors = action.getActionErrors();
|
||||
assertEquals(1, errors.size());
|
||||
String msg = errors.iterator().next();
|
||||
// the error message should contain at least this test
|
||||
assertTrue(msg.startsWith("Der Request übertraf die maximal erlaubte Größe"));
|
||||
assertThat(action.getActionErrors())
|
||||
.hasSize(1)
|
||||
.first()
|
||||
.asString()
|
||||
.startsWith("Der Request übertraf die maximal erlaubte Größe");
|
||||
}
|
||||
|
||||
private String encodeTextFile(String filename, String contentType, String content) {
|
||||
return endline +
|
||||
return endLine +
|
||||
"--" + boundary +
|
||||
endline +
|
||||
"Content-Disposition: form-data; name=\"" + "file" + "\"; filename=\"" + filename +
|
||||
endline +
|
||||
endLine +
|
||||
"Content-Disposition: form-data; name=\"file\"; filename=\"" + filename + "\"" +
|
||||
endLine +
|
||||
"Content-Type: " + contentType +
|
||||
endline +
|
||||
endline +
|
||||
endLine +
|
||||
endLine +
|
||||
content;
|
||||
}
|
||||
|
||||
@@ -568,6 +521,55 @@ public class ActionFileUploadInterceptorTest extends StrutsInternalTestCase {
|
||||
super.tearDown();
|
||||
}
|
||||
|
||||
private UploadedFile createTestFile(File content) {
|
||||
return new UploadedFile() {
|
||||
@Override
|
||||
public Long length() {
|
||||
return 0L;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "";
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isFile() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean delete() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getAbsolutePath() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public File getContent() {
|
||||
return content;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getOriginalName() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getContentType() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getInputName() {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public static class MyFileUploadAction extends ActionSupport implements UploadedFilesAware {
|
||||
private List<UploadedFile> uploadedFiles;
|
||||
|
||||
|
||||
@@ -312,7 +312,7 @@ public class CspInterceptorTest extends StrutsInternalTestCase {
|
||||
public static class CustomDefaultCspSettings extends DefaultCspSettings {
|
||||
|
||||
@Override
|
||||
protected String createPolicyFormat(HttpServletRequest request) {
|
||||
protected String createPolicyFormat(String nonceValue) {
|
||||
return "foo";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,16 +20,19 @@ package org.apache.struts2.interceptor;
|
||||
|
||||
import com.mockobjects.dynamic.ConstraintMatcher;
|
||||
import com.mockobjects.dynamic.Mock;
|
||||
import org.apache.struts2.action.Action;
|
||||
import org.apache.struts2.ActionContext;
|
||||
import org.apache.struts2.ActionInvocation;
|
||||
import org.apache.struts2.ActionSupport;
|
||||
import org.apache.struts2.ModelDriven;
|
||||
import org.apache.struts2.XWorkTestCase;
|
||||
import org.apache.struts2.action.Action;
|
||||
import org.apache.struts2.ognl.ThreadAllowlist;
|
||||
import org.apache.struts2.util.ValueStack;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
|
||||
/**
|
||||
* @author $Author$
|
||||
@@ -40,6 +43,7 @@ public class ModelDrivenInterceptorTest extends XWorkTestCase {
|
||||
Action action;
|
||||
Mock mockActionInvocation;
|
||||
ModelDrivenInterceptor modelDrivenInterceptor;
|
||||
ThreadAllowlist threadAllowlist;
|
||||
Object model;
|
||||
PreResultListener preResultListener;
|
||||
ValueStack stack;
|
||||
@@ -55,6 +59,7 @@ public class ModelDrivenInterceptorTest extends XWorkTestCase {
|
||||
|
||||
Object topOfStack = stack.pop();
|
||||
assertEquals("our model should be on the top of the stack", model, topOfStack);
|
||||
verify(threadAllowlist).allowClassHierarchy(model.getClass());
|
||||
}
|
||||
|
||||
private void setupRefreshModelBeforeResult() {
|
||||
@@ -167,6 +172,8 @@ public class ModelDrivenInterceptorTest extends XWorkTestCase {
|
||||
super.setUp();
|
||||
mockActionInvocation = new Mock(ActionInvocation.class);
|
||||
modelDrivenInterceptor = new ModelDrivenInterceptor();
|
||||
threadAllowlist = mock(ThreadAllowlist.class);
|
||||
modelDrivenInterceptor.setThreadAllowlist(threadAllowlist);
|
||||
stack = ActionContext.getContext().getValueStack();
|
||||
model = new Date(); // any object will do
|
||||
}
|
||||
|
||||
+148
-21
@@ -18,18 +18,26 @@
|
||||
*/
|
||||
package org.apache.struts2.interceptor.parameter;
|
||||
|
||||
import org.aopalliance.intercept.Joinpoint;
|
||||
import org.aopalliance.intercept.MethodInterceptor;
|
||||
import org.apache.commons.lang3.ClassUtils;
|
||||
import org.apache.struts2.ActionContext;
|
||||
import org.apache.struts2.ModelDriven;
|
||||
import org.apache.struts2.StubValueStack;
|
||||
import org.apache.struts2.security.AcceptedPatternsChecker;
|
||||
import org.apache.struts2.security.NotExcludedAcceptedPatternsChecker;
|
||||
import org.apache.commons.lang3.ClassUtils;
|
||||
import org.apache.struts2.dispatcher.HttpParameters;
|
||||
import org.apache.struts2.dispatcher.Parameter;
|
||||
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.ThreadAllowlist;
|
||||
import org.apache.struts2.security.AcceptedPatternsChecker.IsAccepted;
|
||||
import org.apache.struts2.security.ExcludedPatternsChecker.IsExcluded;
|
||||
import org.apache.struts2.security.NotExcludedAcceptedPatternsChecker;
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.springframework.aop.framework.ProxyFactory;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
@@ -37,6 +45,7 @@ import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import static org.apache.struts2.ognl.OgnlCacheFactory.CacheType.LRU;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.Mockito.mock;
|
||||
@@ -56,9 +65,15 @@ public class StrutsParameterAnnotationTest {
|
||||
threadAllowlist = new ThreadAllowlist();
|
||||
parametersInterceptor.setThreadAllowlist(threadAllowlist);
|
||||
|
||||
var ognlUtil = new OgnlUtil(
|
||||
new DefaultOgnlExpressionCacheFactory<>(String.valueOf(1000), LRU.toString()),
|
||||
new DefaultOgnlBeanInfoCacheFactory<>(String.valueOf(1000), LRU.toString()),
|
||||
new StrutsOgnlGuard());
|
||||
parametersInterceptor.setOgnlUtil(ognlUtil);
|
||||
|
||||
NotExcludedAcceptedPatternsChecker checker = mock(NotExcludedAcceptedPatternsChecker.class);
|
||||
when(checker.isAccepted(anyString())).thenReturn(AcceptedPatternsChecker.IsAccepted.yes(""));
|
||||
when(checker.isExcluded(anyString())).thenReturn(NotExcludedAcceptedPatternsChecker.IsExcluded.no(new HashSet<>()));
|
||||
when(checker.isAccepted(anyString())).thenReturn(IsAccepted.yes(""));
|
||||
when(checker.isExcluded(anyString())).thenReturn(IsExcluded.no(Set.of()));
|
||||
parametersInterceptor.setAcceptedPatterns(checker);
|
||||
parametersInterceptor.setExcludedPatterns(checker);
|
||||
}
|
||||
@@ -94,174 +109,267 @@ public class StrutsParameterAnnotationTest {
|
||||
return set;
|
||||
}
|
||||
|
||||
/**
|
||||
* Private String field cannot be injected even when annotated.
|
||||
*/
|
||||
@Test
|
||||
public void privateStrAnnotated() {
|
||||
testParameter(new FieldAction(), "privateStr", false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Public String field can be injected when annotated.
|
||||
*/
|
||||
@Test
|
||||
public void publicStrAnnotated() {
|
||||
testParameter(new FieldAction(), "publicStr", true);
|
||||
assertThat(threadAllowlist.getAllowlist()).isEmpty();
|
||||
}
|
||||
|
||||
/**
|
||||
* Public String field cannot be injected when not annotated.
|
||||
*/
|
||||
@Test
|
||||
public void publicStrNotAnnotated() {
|
||||
testParameter(new FieldAction(), "publicStrNotAnnotated", false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Private Pojo field cannot be injected even when annotated with the appropriate depth.
|
||||
*/
|
||||
@Test
|
||||
public void privatePojoAnnotated() {
|
||||
testParameter(new FieldAction(), "privatePojo.key", false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Public Pojo field cannot be injected when annotated with depth zero.
|
||||
*/
|
||||
@Test
|
||||
public void publicPojoDepthZero() {
|
||||
testParameter(new FieldAction(), "publicPojoDepthZero.key", false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Public Pojo field can be injected when annotated with depth one.
|
||||
*/
|
||||
@Test
|
||||
public void publicPojoDepthOne() {
|
||||
testParameter(new FieldAction(), "publicPojoDepthOne.key", true);
|
||||
assertThat(threadAllowlist.getAllowlist()).containsExactlyInAnyOrderElementsOf(getParentClasses(Pojo.class));
|
||||
}
|
||||
|
||||
/**
|
||||
* Public Pojo field can be injected when annotated with depth one, using the square bracket syntax.
|
||||
*/
|
||||
@Test
|
||||
public void publicPojoDepthOne_sqrBracket() {
|
||||
testParameter(new FieldAction(), "publicPojoDepthOne['key']", true);
|
||||
assertThat(threadAllowlist.getAllowlist()).containsExactlyInAnyOrderElementsOf(getParentClasses(Pojo.class));
|
||||
}
|
||||
|
||||
/**
|
||||
* Public Pojo field can be injected when annotated with depth one, using the bracket syntax.
|
||||
*/
|
||||
@Test
|
||||
public void publicPojoDepthOne_bracket() {
|
||||
testParameter(new FieldAction(), "publicPojoDepthOne('key')", true);
|
||||
assertThat(threadAllowlist.getAllowlist()).containsExactlyInAnyOrderElementsOf(getParentClasses(Pojo.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void publicNestedPojoDepthOne() {
|
||||
testParameter(new FieldAction(), "publicPojoDepthOne.key.key", false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Public Pojo field can be injected when annotated with a depth greater than required.
|
||||
*/
|
||||
@Test
|
||||
public void publicPojoDepthTwo() {
|
||||
testParameter(new FieldAction(), "publicPojoDepthTwo.key", true);
|
||||
assertThat(threadAllowlist.getAllowlist()).containsExactlyInAnyOrderElementsOf(getParentClasses(Pojo.class));
|
||||
}
|
||||
|
||||
/**
|
||||
* Public Pojo field cannot be injected two levels when only annotated with depth one.
|
||||
*/
|
||||
@Test
|
||||
public void publicNestedPojoDepthOne() {
|
||||
testParameter(new FieldAction(), "publicPojoDepthOne.key.key", false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Public Pojo field can be injected two levels when annotated with depth two.
|
||||
*/
|
||||
@Test
|
||||
public void publicNestedPojoDepthTwo() {
|
||||
testParameter(new FieldAction(), "publicPojoDepthTwo.key.key", true);
|
||||
assertThat(threadAllowlist.getAllowlist()).containsExactlyInAnyOrderElementsOf(getParentClasses(Pojo.class));
|
||||
}
|
||||
|
||||
/**
|
||||
* Public Pojo field can be injected two levels when annotated with depth two, using the square bracket syntax.
|
||||
*/
|
||||
@Test
|
||||
public void publicNestedPojoDepthTwo_sqrBracket() {
|
||||
testParameter(new FieldAction(), "publicPojoDepthTwo['key']['key']", true);
|
||||
assertThat(threadAllowlist.getAllowlist()).containsExactlyInAnyOrderElementsOf(getParentClasses(Pojo.class));
|
||||
}
|
||||
|
||||
/**
|
||||
* Public Pojo field can be injected two levels when annotated with depth two, using the bracket syntax.
|
||||
*/
|
||||
@Test
|
||||
public void publicNestedPojoDepthTwo_bracket() {
|
||||
testParameter(new FieldAction(), "publicPojoDepthTwo('key')('key')", true);
|
||||
assertThat(threadAllowlist.getAllowlist()).containsExactlyInAnyOrderElementsOf(getParentClasses(Pojo.class));
|
||||
}
|
||||
|
||||
/**
|
||||
* Private String setting method cannot be injected even when annotated.
|
||||
*/
|
||||
@Test
|
||||
public void privateStrAnnotatedMethod() {
|
||||
testParameter(new MethodAction(), "privateStr", false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Public String setting method can be injected when annotated.
|
||||
*/
|
||||
@Test
|
||||
public void publicStrAnnotatedMethod() {
|
||||
testParameter(new MethodAction(), "publicStr", true);
|
||||
assertThat(threadAllowlist.getAllowlist()).isEmpty();
|
||||
}
|
||||
|
||||
/**
|
||||
* Public String setting method cannot be injected when not annotated.
|
||||
*/
|
||||
@Test
|
||||
public void publicStrNotAnnotatedMethod() {
|
||||
testParameter(new MethodAction(), "publicStrNotAnnotated", false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Private Pojo returning method cannot be injected even when annotated with the appropriate depth.
|
||||
*/
|
||||
@Test
|
||||
public void privatePojoAnnotatedMethod() {
|
||||
testParameter(new MethodAction(), "privatePojo.key", false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Public Pojo returning method cannot be injected when annotated with depth zero.
|
||||
*/
|
||||
@Test
|
||||
public void publicPojoDepthZeroMethod() {
|
||||
testParameter(new MethodAction(), "publicPojoDepthZero.key", false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Public Pojo returning method can be injected when annotated with depth one.
|
||||
*/
|
||||
@Test
|
||||
public void publicPojoDepthOneMethod() {
|
||||
testParameter(new MethodAction(), "publicPojoDepthOne.key", true);
|
||||
assertThat(threadAllowlist.getAllowlist()).containsExactlyInAnyOrderElementsOf(getParentClasses(Pojo.class));
|
||||
}
|
||||
|
||||
/**
|
||||
* Public Pojo returning method cannot be injected two levels when only annotated with depth one.
|
||||
*/
|
||||
@Test
|
||||
public void publicNestedPojoDepthOneMethod() {
|
||||
testParameter(new MethodAction(), "publicPojoDepthOne.key.key", false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Public Pojo returning method can be injected when annotated with a depth greater than required.
|
||||
*/
|
||||
@Test
|
||||
public void publicPojoDepthTwoMethod() {
|
||||
testParameter(new MethodAction(), "publicPojoDepthTwo.key", true);
|
||||
assertThat(threadAllowlist.getAllowlist()).containsExactlyInAnyOrderElementsOf(getParentClasses(Pojo.class));
|
||||
}
|
||||
|
||||
/**
|
||||
* Public Pojo returning method can be injected two levels when annotated with depth two.
|
||||
*/
|
||||
@Test
|
||||
public void publicNestedPojoDepthTwoMethod() {
|
||||
testParameter(new MethodAction(), "publicPojoDepthTwo.key.key", true);
|
||||
assertThat(threadAllowlist.getAllowlist()).containsExactlyInAnyOrderElementsOf(getParentClasses(Pojo.class));
|
||||
}
|
||||
|
||||
/**
|
||||
* Public list of Pojo field cannot be injected when annotated with depth one.
|
||||
*/
|
||||
@Test
|
||||
public void publicPojoListDepthOne() {
|
||||
testParameter(new FieldAction(), "publicPojoListDepthOne[0].key", false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Public list of Pojo field can be injected when annotated with depth two.
|
||||
*/
|
||||
@Test
|
||||
public void publicPojoListDepthTwo() {
|
||||
testParameter(new FieldAction(), "publicPojoListDepthTwo[0].key", true);
|
||||
assertThat(threadAllowlist.getAllowlist()).containsExactlyInAnyOrderElementsOf(getParentClasses(List.class, Pojo.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void publicPojoMapDepthTwo() {
|
||||
testParameter(new FieldAction(), "publicPojoMapDepthTwo['a'].key", true);
|
||||
assertThat(threadAllowlist.getAllowlist()).containsExactlyInAnyOrderElementsOf(getParentClasses(Map.class, String.class, Pojo.class));
|
||||
}
|
||||
|
||||
/**
|
||||
* Public list of Pojo returning method cannot be injected when annotated with depth one.
|
||||
*/
|
||||
@Test
|
||||
public void publicPojoListDepthOneMethod() {
|
||||
testParameter(new MethodAction(), "publicPojoListDepthOne[0].key", false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Public list of Pojo returning method can be injected when annotated with depth two.
|
||||
*/
|
||||
@Test
|
||||
public void publicPojoListDepthTwoMethod() {
|
||||
testParameter(new MethodAction(), "publicPojoListDepthTwo[0].key", true);
|
||||
assertThat(threadAllowlist.getAllowlist()).containsExactlyInAnyOrderElementsOf(getParentClasses(List.class, Pojo.class));
|
||||
}
|
||||
|
||||
/**
|
||||
* Public map of Pojo field can be injected when annotated with depth two.
|
||||
*/
|
||||
@Test
|
||||
public void publicPojoMapDepthTwo() {
|
||||
testParameter(new FieldAction(), "publicPojoMapDepthTwo['a'].key", true);
|
||||
assertThat(threadAllowlist.getAllowlist()).containsExactlyInAnyOrderElementsOf(getParentClasses(Map.class, String.class, Pojo.class));
|
||||
}
|
||||
|
||||
/**
|
||||
* Public map of Pojo returning method can be injected when annotated with depth two.
|
||||
*/
|
||||
@Test
|
||||
public void publicPojoMapDepthTwoMethod() {
|
||||
testParameter(new MethodAction(), "publicPojoMapDepthTwo['a'].key", true);
|
||||
assertThat(threadAllowlist.getAllowlist()).containsExactlyInAnyOrderElementsOf(getParentClasses(Map.class, String.class, Pojo.class));
|
||||
}
|
||||
|
||||
/**
|
||||
* Public String field can be injected even when not annotated, if transition mode is enabled.
|
||||
*/
|
||||
@Test
|
||||
public void publicStrNotAnnotated_transitionMode() {
|
||||
parametersInterceptor.setRequireAnnotationsTransitionMode(Boolean.TRUE.toString());
|
||||
testParameter(new FieldAction(), "publicStrNotAnnotated", true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Public String setting method can be injected even when not annotated, if transition mode is enabled.
|
||||
*/
|
||||
@Test
|
||||
public void publicStrNotAnnotatedMethod_transitionMode() {
|
||||
parametersInterceptor.setRequireAnnotationsTransitionMode(Boolean.TRUE.toString());
|
||||
testParameter(new MethodAction(), "publicStrNotAnnotated", true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Models of ModelDriven actions can be injected without any annotations on the Action.
|
||||
*/
|
||||
@Test
|
||||
public void publicModelPojo() {
|
||||
var action = new ModelAction();
|
||||
@@ -273,10 +381,29 @@ public class StrutsParameterAnnotationTest {
|
||||
|
||||
testParameter(action, "name", true);
|
||||
testParameter(action, "name.nested", true);
|
||||
assertThat(threadAllowlist.getAllowlist()).containsExactlyInAnyOrderElementsOf(getParentClasses(Object.class, Pojo.class));
|
||||
}
|
||||
|
||||
static class FieldAction {
|
||||
/**
|
||||
* Models of ModelDriven actions can be injected without any annotations on the Action, even when the Action is
|
||||
* proxied.
|
||||
*/
|
||||
@Test
|
||||
public void publicModelPojo_proxied() {
|
||||
var proxyFactory = new ProxyFactory(new ModelAction());
|
||||
proxyFactory.setProxyTargetClass(true);
|
||||
proxyFactory.addAdvice((MethodInterceptor) Joinpoint::proceed);
|
||||
var proxiedAction = (ModelAction) proxyFactory.getProxy();
|
||||
|
||||
// Emulate ModelDrivenInterceptor running previously
|
||||
var valueStack = new StubValueStack();
|
||||
valueStack.push(proxiedAction.getModel());
|
||||
ActionContext.of().withValueStack(valueStack).bind();
|
||||
|
||||
testParameter(proxiedAction, "name", true);
|
||||
testParameter(proxiedAction, "name.nested", true);
|
||||
}
|
||||
|
||||
public static class FieldAction {
|
||||
@StrutsParameter
|
||||
private String privateStr;
|
||||
|
||||
@@ -307,7 +434,7 @@ public class StrutsParameterAnnotationTest {
|
||||
public Map<String, Pojo> publicPojoMapDepthTwo;
|
||||
}
|
||||
|
||||
static class MethodAction {
|
||||
public static class MethodAction {
|
||||
|
||||
@StrutsParameter
|
||||
private void setPrivateStr(String str) {
|
||||
@@ -360,7 +487,7 @@ public class StrutsParameterAnnotationTest {
|
||||
}
|
||||
}
|
||||
|
||||
static class ModelAction implements ModelDriven<Pojo> {
|
||||
public static class ModelAction implements ModelDriven<Pojo> {
|
||||
|
||||
@Override
|
||||
public Pojo getModel() {
|
||||
@@ -368,6 +495,6 @@ public class StrutsParameterAnnotationTest {
|
||||
}
|
||||
}
|
||||
|
||||
static class Pojo {
|
||||
public static class Pojo {
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,8 +18,18 @@
|
||||
*/
|
||||
package org.apache.struts2.ognl;
|
||||
|
||||
import ognl.InappropriateExpressionException;
|
||||
import ognl.MethodFailedException;
|
||||
import ognl.NoSuchPropertyException;
|
||||
import ognl.NullHandler;
|
||||
import ognl.Ognl;
|
||||
import ognl.OgnlContext;
|
||||
import ognl.OgnlException;
|
||||
import ognl.OgnlRuntime;
|
||||
import ognl.SimpleNode;
|
||||
import org.apache.struts2.ActionContext;
|
||||
import org.apache.struts2.text.StubTextProvider;
|
||||
import org.apache.struts2.StrutsConstants;
|
||||
import org.apache.struts2.StrutsException;
|
||||
import org.apache.struts2.StubValueStack;
|
||||
import org.apache.struts2.XWorkTestCase;
|
||||
import org.apache.struts2.config.ConfigurationException;
|
||||
@@ -30,6 +40,7 @@ import org.apache.struts2.ognl.accessor.CompoundRootAccessor;
|
||||
import org.apache.struts2.ognl.accessor.RootAccessor;
|
||||
import org.apache.struts2.test.StubConfigurationProvider;
|
||||
import org.apache.struts2.test.User;
|
||||
import org.apache.struts2.text.StubTextProvider;
|
||||
import org.apache.struts2.util.Bar;
|
||||
import org.apache.struts2.util.CompoundRoot;
|
||||
import org.apache.struts2.util.Foo;
|
||||
@@ -37,20 +48,11 @@ import org.apache.struts2.util.Owner;
|
||||
import org.apache.struts2.util.ValueStack;
|
||||
import org.apache.struts2.util.location.LocatableProperties;
|
||||
import org.apache.struts2.util.reflection.ReflectionContextState;
|
||||
import ognl.InappropriateExpressionException;
|
||||
import ognl.MethodFailedException;
|
||||
import ognl.NoSuchPropertyException;
|
||||
import ognl.NullHandler;
|
||||
import ognl.Ognl;
|
||||
import ognl.OgnlContext;
|
||||
import ognl.OgnlException;
|
||||
import ognl.OgnlRuntime;
|
||||
import ognl.SimpleNode;
|
||||
import org.apache.struts2.StrutsConstants;
|
||||
import org.apache.struts2.StrutsException;
|
||||
import org.mockito.MockedStatic;
|
||||
|
||||
import java.beans.BeanInfo;
|
||||
import java.beans.IntrospectionException;
|
||||
import java.beans.Introspector;
|
||||
import java.lang.reflect.Method;
|
||||
import java.text.DateFormat;
|
||||
import java.util.ArrayList;
|
||||
@@ -66,6 +68,7 @@ import static org.apache.struts2.ognl.OgnlCacheFactory.CacheType.BASIC;
|
||||
import static org.apache.struts2.ognl.OgnlCacheFactory.CacheType.LRU;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.junit.Assert.assertThrows;
|
||||
import static org.mockito.Mockito.mockStatic;
|
||||
|
||||
public class OgnlUtilTest extends XWorkTestCase {
|
||||
|
||||
@@ -432,6 +435,17 @@ public class OgnlUtilTest extends XWorkTestCase {
|
||||
assertNotSame("BeanInfo dropped from LRU cache is the same as newly added ?", beanInfo1_1, beanInfo1_4);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure any {@link IntrospectionException} thrown by {@link Introspector} are propagated as is.
|
||||
*/
|
||||
public void testBeanInfoCacheExceptionHandling() {
|
||||
try (MockedStatic<Introspector> introspector = mockStatic(Introspector.class)) {
|
||||
var exception = new IntrospectionException("Test Exception");
|
||||
introspector.when(() -> Introspector.getBeanInfo(TestBean1.class, Object.class)).thenThrow(exception);
|
||||
assertSame(exception, assertThrows(IntrospectionException.class, () -> ognlUtil.getBeanInfo(new TestBean1())));
|
||||
}
|
||||
}
|
||||
|
||||
public void testClearRuntimeCache() {
|
||||
// Confirm that no exceptions or failures arise when calling the convenience global clear method.
|
||||
OgnlUtil.clearRuntimeCache();
|
||||
|
||||
@@ -18,12 +18,12 @@
|
||||
*/
|
||||
package org.apache.struts2.ognl;
|
||||
|
||||
import ognl.MemberAccess;
|
||||
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.Foo;
|
||||
import ognl.MemberAccess;
|
||||
import org.apache.commons.lang3.reflect.FieldUtils;
|
||||
import org.hibernate.proxy.HibernateProxy;
|
||||
import org.hibernate.proxy.LazyInitializer;
|
||||
import org.junit.Before;
|
||||
|
||||
@@ -18,7 +18,6 @@
|
||||
*/
|
||||
package org.apache.struts2.result;
|
||||
|
||||
import jakarta.servlet.RequestDispatcher;
|
||||
import org.apache.struts2.ActionContext;
|
||||
import org.apache.struts2.StrutsInternalTestCase;
|
||||
import org.apache.struts2.StrutsStatics;
|
||||
@@ -39,30 +38,38 @@ public class ServletDispatcherResultTest extends StrutsInternalTestCase implemen
|
||||
ServletDispatcherResult view = new ServletDispatcherResult();
|
||||
view.setLocation("foo.jsp");
|
||||
|
||||
request.setRequestURI("/app/namespace/my.action");
|
||||
request.setContextPath("/app");
|
||||
request.setServletPath("/namespace/my.action");
|
||||
request.setPathInfo(null);
|
||||
request.setQueryString("a=1&b=2");
|
||||
|
||||
request.setAttribute("struts.actiontag.invocation", null);
|
||||
request.setAttribute("jakarta.servlet.include.servlet_path", null);
|
||||
request.setRequestURI("foo.jsp");
|
||||
|
||||
response.setCommitted(Boolean.FALSE);
|
||||
|
||||
view.execute(invocation);
|
||||
|
||||
assertEquals("foo.jsp", response.getForwardedUrl());
|
||||
assertEquals("foo.jsp", request.getAttribute(RequestDispatcher.FORWARD_SERVLET_PATH));
|
||||
}
|
||||
|
||||
public void testInclude() throws Exception {
|
||||
ServletDispatcherResult view = new ServletDispatcherResult();
|
||||
view.setLocation("foo.jsp");
|
||||
|
||||
request.setRequestURI("/app/namespace/my.action");
|
||||
request.setContextPath("/app");
|
||||
request.setServletPath("/namespace/my.action");
|
||||
request.setPathInfo(null);
|
||||
request.setQueryString("a=1&b=2");
|
||||
|
||||
request.setAttribute("struts.actiontag.invocation", null);
|
||||
response.setCommitted(Boolean.TRUE);
|
||||
request.setRequestURI("foo.jsp");
|
||||
|
||||
view.execute(invocation);
|
||||
|
||||
assertEquals("foo.jsp", response.getIncludedUrl());
|
||||
assertEquals("foo.jsp", request.getAttribute(RequestDispatcher.FORWARD_SERVLET_PATH));
|
||||
}
|
||||
|
||||
public void testWithParameter() throws Exception {
|
||||
@@ -76,7 +83,6 @@ public class ServletDispatcherResultTest extends StrutsInternalTestCase implemen
|
||||
|
||||
// See https://issues.apache.org/jira/browse/WW-5486
|
||||
assertEquals("1", stack.findString("#parameters.bar"));
|
||||
assertEquals("foo.jsp", request.getAttribute(RequestDispatcher.FORWARD_SERVLET_PATH));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
+2
-1
@@ -24,7 +24,8 @@
|
||||
<parent>
|
||||
<groupId>org.apache.struts</groupId>
|
||||
<artifactId>struts2-parent</artifactId>
|
||||
<version>7.0.3</version>
|
||||
<version>7.1.0</version>
|
||||
<relativePath>../parent/pom.xml</relativePath>
|
||||
</parent>
|
||||
<artifactId>struts2-jakarta</artifactId>
|
||||
<packaging>pom</packaging>
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
<parent>
|
||||
<groupId>org.apache.struts</groupId>
|
||||
<artifactId>struts2-jakarta</artifactId>
|
||||
<version>7.0.3</version>
|
||||
<version>7.1.0</version>
|
||||
</parent>
|
||||
<artifactId>struts2-velocity-tools-jsp-jakarta</artifactId>
|
||||
<packaging>jar</packaging>
|
||||
@@ -41,7 +41,7 @@
|
||||
<plugin>
|
||||
<groupId>org.eclipse.transformer</groupId>
|
||||
<artifactId>transformer-maven-plugin</artifactId>
|
||||
<version>0.5.0</version>
|
||||
<version>1.0.0</version>
|
||||
<extensions>true</extensions>
|
||||
<configuration>
|
||||
<rules>
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
<parent>
|
||||
<groupId>org.apache.struts</groupId>
|
||||
<artifactId>struts2-jakarta</artifactId>
|
||||
<version>7.0.3</version>
|
||||
<version>7.1.0</version>
|
||||
</parent>
|
||||
<artifactId>struts2-velocity-tools-view-jakarta</artifactId>
|
||||
<packaging>jar</packaging>
|
||||
@@ -41,7 +41,7 @@
|
||||
<plugin>
|
||||
<groupId>org.eclipse.transformer</groupId>
|
||||
<artifactId>transformer-maven-plugin</artifactId>
|
||||
<version>0.5.0</version>
|
||||
<version>1.0.0</version>
|
||||
<extensions>true</extensions>
|
||||
<configuration>
|
||||
<rules>
|
||||
|
||||
+305
@@ -0,0 +1,305 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
-->
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<parent>
|
||||
<groupId>org.apache.struts</groupId>
|
||||
<artifactId>struts2-bom</artifactId>
|
||||
<version>7.1.0</version>
|
||||
<relativePath>../bom/pom.xml</relativePath>
|
||||
</parent>
|
||||
|
||||
<artifactId>struts2-parent</artifactId>
|
||||
<packaging>pom</packaging>
|
||||
<name>Struts Parent POM</name>
|
||||
<description>Apache Struts</description>
|
||||
|
||||
<properties>
|
||||
<maven.site.skip>true</maven.site.skip>
|
||||
<maven.site.deploy.skip>true</maven.site.deploy.skip>
|
||||
</properties>
|
||||
|
||||
<dependencyManagement>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.apache.struts</groupId>
|
||||
<artifactId>struts-annotations</artifactId>
|
||||
<version>2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.freemarker</groupId>
|
||||
<artifactId>freemarker</artifactId>
|
||||
<version>${freemarker.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.github.ben-manes.caffeine</groupId>
|
||||
<artifactId>caffeine</artifactId>
|
||||
<version>3.2.2</version>
|
||||
</dependency>
|
||||
|
||||
<!-- Velocity -->
|
||||
<dependency>
|
||||
<groupId>org.apache.velocity</groupId>
|
||||
<artifactId>velocity-engine-core</artifactId>
|
||||
<version>2.4.1</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.velocity.tools</groupId>
|
||||
<artifactId>velocity-tools-generic</artifactId>
|
||||
<version>${velocity-tools.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.struts</groupId>
|
||||
<artifactId>struts2-velocity-tools-view-jakarta</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.struts</groupId>
|
||||
<artifactId>struts2-velocity-tools-jsp-jakarta</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>ognl</groupId>
|
||||
<artifactId>ognl</artifactId>
|
||||
<version>${ognl.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.ow2.asm</groupId>
|
||||
<artifactId>asm</artifactId>
|
||||
<version>${asm.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.ow2.asm</groupId>
|
||||
<artifactId>asm-commons</artifactId>
|
||||
<version>${asm.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>jakarta.platform</groupId>
|
||||
<artifactId>jakarta.jakartaee-bom</artifactId>
|
||||
<version>${jakarta-ee.version}</version>
|
||||
<type>pom</type>
|
||||
<scope>import</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.glassfish.jaxb</groupId>
|
||||
<artifactId>jaxb-bom</artifactId>
|
||||
<version>${jaxb-impl.version}</version>
|
||||
<type>pom</type>
|
||||
<scope>import</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.glassfish</groupId>
|
||||
<artifactId>jakarta.el</artifactId>
|
||||
<version>5.0.0-M1</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<!-- Commons -->
|
||||
<dependency>
|
||||
<groupId>commons-logging</groupId>
|
||||
<artifactId>commons-logging</artifactId>
|
||||
<version>1.3.4</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.commons</groupId>
|
||||
<artifactId>commons-collections4</artifactId>
|
||||
<version>4.5.0</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.commons</groupId>
|
||||
<artifactId>commons-fileupload2-jakarta-servlet6</artifactId>
|
||||
<version>2.0.0-M4</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>commons-io</groupId>
|
||||
<artifactId>commons-io</artifactId>
|
||||
<version>2.18.0</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.commons</groupId>
|
||||
<artifactId>commons-lang3</artifactId>
|
||||
<version>3.18.0</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.commons</groupId>
|
||||
<artifactId>commons-text</artifactId>
|
||||
<version>1.14.0</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.commons</groupId>
|
||||
<artifactId>commons-jci-fam</artifactId>
|
||||
<version>1.1</version>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>commons-beanutils</groupId>
|
||||
<artifactId>commons-beanutils</artifactId>
|
||||
<version>1.11.0</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>commons-validator</groupId>
|
||||
<artifactId>commons-validator</artifactId>
|
||||
<version>1.10.0</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.commons</groupId>
|
||||
<artifactId>commons-compress</artifactId>
|
||||
<version>1.28.0</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.commons</groupId>
|
||||
<artifactId>commons-digester3</artifactId>
|
||||
<version>3.2</version>
|
||||
<exclusions>
|
||||
<exclusion>
|
||||
<groupId>commons-logging</groupId>
|
||||
<artifactId>commons-logging</artifactId>
|
||||
</exclusion>
|
||||
<exclusion>
|
||||
<groupId>asm</groupId>
|
||||
<artifactId>asm</artifactId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework</groupId>
|
||||
<artifactId>spring-framework-bom</artifactId>
|
||||
<version>${spring.version}</version>
|
||||
<type>pom</type>
|
||||
<scope>import</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>junit</groupId>
|
||||
<artifactId>junit</artifactId>
|
||||
<version>4.13.2</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.easymock</groupId>
|
||||
<artifactId>easymock</artifactId>
|
||||
<version>5.6.0</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.awaitility</groupId>
|
||||
<artifactId>awaitility</artifactId>
|
||||
<version>4.3.0</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>mockobjects</groupId>
|
||||
<artifactId>mockobjects-core</artifactId>
|
||||
<version>0.09</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.htmlunit</groupId>
|
||||
<artifactId>htmlunit</artifactId>
|
||||
<version>4.16.0</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>jmock</groupId>
|
||||
<artifactId>jmock</artifactId>
|
||||
<version>1.2.0</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.assertj</groupId>
|
||||
<artifactId>assertj-core</artifactId>
|
||||
<version>3.27.4</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.mockito</groupId>
|
||||
<artifactId>mockito-core</artifactId>
|
||||
<version>${mockito.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>net.bytebuddy</groupId>
|
||||
<artifactId>byte-buddy</artifactId>
|
||||
<version>${byte-buddy.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>net.bytebuddy</groupId>
|
||||
<artifactId>byte-buddy-agent</artifactId>
|
||||
<version>${byte-buddy.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.testng</groupId>
|
||||
<artifactId>testng</artifactId>
|
||||
<version>7.11.0</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.slf4j</groupId>
|
||||
<artifactId>slf4j-api</artifactId>
|
||||
<version>${slf4j.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.slf4j</groupId>
|
||||
<artifactId>slf4j-simple</artifactId>
|
||||
<version>${slf4j.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.apache.logging.log4j</groupId>
|
||||
<artifactId>log4j-bom</artifactId>
|
||||
<version>${log4j2.version}</version>
|
||||
<type>pom</type>
|
||||
<scope>import</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.thoughtworks.xstream</groupId>
|
||||
<artifactId>xstream</artifactId>
|
||||
<version>1.4.21</version>
|
||||
<exclusions>
|
||||
<exclusion>
|
||||
<groupId>io.github.x-stream</groupId>
|
||||
<artifactId>mxparser</artifactId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.fasterxml.jackson</groupId>
|
||||
<artifactId>jackson-bom</artifactId>
|
||||
<version>${jackson.version}</version>
|
||||
<type>pom</type>
|
||||
<scope>import</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</dependencyManagement>
|
||||
</project>
|
||||
@@ -24,7 +24,7 @@
|
||||
<parent>
|
||||
<groupId>org.apache.struts</groupId>
|
||||
<artifactId>struts2-plugins</artifactId>
|
||||
<version>7.0.3</version>
|
||||
<version>7.1.0</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>struts2-async-plugin</artifactId>
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
<parent>
|
||||
<groupId>org.apache.struts</groupId>
|
||||
<artifactId>struts2-plugins</artifactId>
|
||||
<version>7.0.3</version>
|
||||
<version>7.1.0</version>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
@@ -37,11 +37,9 @@
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
|
||||
<dependency>
|
||||
<groupId>jakarta.validation</groupId>
|
||||
<artifactId>jakarta.validation-api</artifactId>
|
||||
<version>3.1.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
@@ -55,29 +53,24 @@
|
||||
<artifactId>jakarta.el</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>javax.xml.bind</groupId>
|
||||
<artifactId>jaxb-api</artifactId>
|
||||
<version>2.3.1</version>
|
||||
<groupId>jakarta.xml.bind</groupId>
|
||||
<artifactId>jakarta.xml.bind-api</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.sun.xml.bind</groupId>
|
||||
<artifactId>jaxb-core</artifactId>
|
||||
<version>2.3.0.1</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.sun.xml.bind</groupId>
|
||||
<artifactId>jaxb-impl</artifactId>
|
||||
<version>2.3.3</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>javax.activation</groupId>
|
||||
<artifactId>activation</artifactId>
|
||||
<version>1.1.1</version>
|
||||
<groupId>jakarta.activation</groupId>
|
||||
<artifactId>jakarta.activation-api</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
|
||||
+12
-11
@@ -25,7 +25,7 @@
|
||||
<parent>
|
||||
<groupId>org.apache.struts</groupId>
|
||||
<artifactId>struts2-plugins</artifactId>
|
||||
<version>7.0.3</version>
|
||||
<version>7.1.0</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>struts2-cdi-plugin</artifactId>
|
||||
@@ -33,7 +33,6 @@
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<dependencies>
|
||||
|
||||
<dependency>
|
||||
<groupId>jakarta.enterprise</groupId>
|
||||
<artifactId>jakarta.enterprise.cdi-api</artifactId>
|
||||
@@ -43,29 +42,31 @@
|
||||
<dependency>
|
||||
<groupId>org.jboss.weld</groupId>
|
||||
<artifactId>weld-core-impl</artifactId>
|
||||
<scope>provided</scope>
|
||||
<version>${weld.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.jboss.weld.se</groupId>
|
||||
<artifactId>weld-se-core</artifactId>
|
||||
<version>${weld.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework</groupId>
|
||||
<artifactId>spring-test</artifactId>
|
||||
<!-- org.springframework.mock.jndi.SimpleNamingContextBuilder removed from newer version -->
|
||||
<version>4.3.0.RELEASE</version>
|
||||
<groupId>com.github.h-thurow</groupId>
|
||||
<artifactId>simple-jndi</artifactId>
|
||||
<version>0.25.0</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>commons-logging</groupId>
|
||||
<artifactId>commons-logging</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>jakarta.inject</groupId>
|
||||
<artifactId>jakarta.inject-api</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<properties>
|
||||
|
||||
@@ -21,11 +21,14 @@ package org.apache.struts2.cdi;
|
||||
import org.jboss.weld.bootstrap.api.helpers.RegistrySingletonProvider;
|
||||
import org.jboss.weld.environment.se.Weld;
|
||||
import org.jboss.weld.environment.se.WeldContainer;
|
||||
import org.junit.AfterClass;
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.Test;
|
||||
import org.springframework.mock.jndi.SimpleNamingContextBuilder;
|
||||
|
||||
import jakarta.enterprise.inject.spi.InjectionTarget;
|
||||
import javax.naming.Context;
|
||||
import javax.naming.InitialContext;
|
||||
import javax.naming.NamingException;
|
||||
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.junit.Assert.assertSame;
|
||||
@@ -33,14 +36,28 @@ import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class CdiObjectFactoryTest {
|
||||
|
||||
private static final String SHARED_JNDI = "org.osjava.sj.jndi.shared";
|
||||
private static InitialContext context;
|
||||
private static WeldContainer container;
|
||||
|
||||
@BeforeClass
|
||||
public static void setup() throws Exception {
|
||||
Weld weld = new Weld().containerId(RegistrySingletonProvider.STATIC_INSTANCE);
|
||||
WeldContainer container = weld.initialize();
|
||||
container = new Weld().containerId(RegistrySingletonProvider.STATIC_INSTANCE).initialize();
|
||||
|
||||
SimpleNamingContextBuilder builder = new SimpleNamingContextBuilder();
|
||||
builder.activate();
|
||||
builder.bind(CdiObjectFactory.CDI_JNDIKEY_BEANMANAGER_COMP, container.getBeanManager());
|
||||
System.setProperty(Context.INITIAL_CONTEXT_FACTORY, "org.osjava.sj.SimpleContextFactory");
|
||||
System.setProperty(SHARED_JNDI, "true");
|
||||
context = new InitialContext();
|
||||
context.bind(CdiObjectFactory.CDI_JNDIKEY_BEANMANAGER_COMP, container.getBeanManager());
|
||||
}
|
||||
|
||||
@AfterClass
|
||||
public static void tearDown() throws NamingException {
|
||||
container.shutdown();
|
||||
|
||||
context.unbind(CdiObjectFactory.CDI_JNDIKEY_BEANMANAGER_COMP);
|
||||
context.close();
|
||||
System.clearProperty(Context.INITIAL_CONTEXT_FACTORY);
|
||||
System.clearProperty(SHARED_JNDI);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -50,16 +67,16 @@ public class CdiObjectFactoryTest {
|
||||
|
||||
@Test
|
||||
public void testGetBean() throws Exception {
|
||||
final CdiObjectFactory cdiObjectFactory = new CdiObjectFactory();
|
||||
FooConsumer fooConsumer = (FooConsumer) cdiObjectFactory.buildBean(FooConsumer.class.getCanonicalName(), null, false);
|
||||
var cdiObjectFactory = new CdiObjectFactory();
|
||||
var fooConsumer = (FooConsumer) cdiObjectFactory.buildBean(FooConsumer.class.getCanonicalName(), null, false);
|
||||
assertNotNull(fooConsumer);
|
||||
assertNotNull(fooConsumer.fooService);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetInjectionTarget() {
|
||||
final CdiObjectFactory cdiObjectFactory = new CdiObjectFactory();
|
||||
final InjectionTarget<?> injectionTarget = cdiObjectFactory.getInjectionTarget(FooConsumer.class);
|
||||
var cdiObjectFactory = new CdiObjectFactory();
|
||||
InjectionTarget<?> injectionTarget = cdiObjectFactory.getInjectionTarget(FooConsumer.class);
|
||||
assertNotNull(injectionTarget);
|
||||
assertTrue(cdiObjectFactory.injectionTargetCache.containsKey(FooConsumer.class));
|
||||
assertSame(cdiObjectFactory.getInjectionTarget(FooConsumer.class), injectionTarget);
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
<parent>
|
||||
<groupId>org.apache.struts</groupId>
|
||||
<artifactId>struts2-plugins</artifactId>
|
||||
<version>7.0.3</version>
|
||||
<version>7.1.0</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>struts2-config-browser-plugin</artifactId>
|
||||
|
||||
+5
-10
@@ -25,6 +25,7 @@ import org.apache.struts2.util.reflection.ReflectionProvider;
|
||||
import org.apache.struts2.validator.Validator;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.apache.struts2.ActionContext;
|
||||
import org.apache.struts2.interceptor.parameter.StrutsParameter;
|
||||
|
||||
import java.beans.BeanInfo;
|
||||
@@ -49,19 +50,13 @@ public class ShowValidatorAction extends ListValidatorsAction {
|
||||
private Set<PropertyInfo> properties = Collections.emptySet();
|
||||
private int selected = 0;
|
||||
|
||||
ReflectionProvider reflectionProvider;
|
||||
ReflectionContextFactory reflectionContextFactory;
|
||||
private ReflectionProvider reflectionProvider;
|
||||
|
||||
@Inject
|
||||
public void setReflectionProvider(ReflectionProvider prov) {
|
||||
this.reflectionProvider = prov;
|
||||
}
|
||||
|
||||
@Inject
|
||||
public void setReflectionContextFactory(ReflectionContextFactory fac) {
|
||||
this.reflectionContextFactory = fac;
|
||||
}
|
||||
|
||||
public int getSelected() {
|
||||
return selected;
|
||||
}
|
||||
@@ -85,7 +80,6 @@ public class ShowValidatorAction extends ListValidatorsAction {
|
||||
Validator validator = getSelectedValidator();
|
||||
properties = new TreeSet<>();
|
||||
try {
|
||||
Map<String, Object> context = reflectionContextFactory.createDefaultContext(validator);
|
||||
BeanInfo beanInfoFrom;
|
||||
try {
|
||||
beanInfoFrom = Introspector.getBeanInfo(validator.getClass(), Object.class);
|
||||
@@ -97,6 +91,7 @@ public class ShowValidatorAction extends ListValidatorsAction {
|
||||
|
||||
PropertyDescriptor[] pds = beanInfoFrom.getPropertyDescriptors();
|
||||
|
||||
Map<String, Object> context = ActionContext.getContext().getContextMap();
|
||||
for (PropertyDescriptor pd : pds) {
|
||||
String name = pd.getName();
|
||||
Object value = null;
|
||||
@@ -113,9 +108,9 @@ public class ShowValidatorAction extends ListValidatorsAction {
|
||||
}
|
||||
} catch (Exception e) {
|
||||
if (LOG.isWarnEnabled()) {
|
||||
LOG.warn("Unable to retrieve properties.", e);
|
||||
LOG.warn("Unable to retrieve properties.", e);
|
||||
}
|
||||
addActionError("Unable to retrieve properties: " + e.toString());
|
||||
addActionError("Unable to retrieve properties: " + e);
|
||||
}
|
||||
|
||||
if (hasErrors()) {
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
<parent>
|
||||
<groupId>org.apache.struts</groupId>
|
||||
<artifactId>struts2-plugins</artifactId>
|
||||
<version>7.0.3</version>
|
||||
<version>7.1.0</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>struts2-convention-plugin</artifactId>
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
<parent>
|
||||
<groupId>org.apache.struts</groupId>
|
||||
<artifactId>struts2-plugins</artifactId>
|
||||
<version>7.0.3</version>
|
||||
<version>7.1.0</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>struts2-jasperreports-plugin</artifactId>
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
# Jasper Reports plugin
|
||||
This plugin allows to use Jasper reports as a one of the result types.
|
||||
You will find more details in [documentation](https://struts.apache.org/plugins/jasperreports/).
|
||||
|
||||
## Installation
|
||||
Just drop this plugin JAR into `WEB-INF/lib` folder or add it as a Maven dependency.
|
||||
@@ -0,0 +1,82 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
-->
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<parent>
|
||||
<groupId>org.apache.struts</groupId>
|
||||
<artifactId>struts2-plugins</artifactId>
|
||||
<version>7.1.0</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>struts2-jasperreports7-plugin</artifactId>
|
||||
<packaging>jar</packaging>
|
||||
<name>Struts 2 Jasper Reports 7 Plugin [EXPERIMENTAL]</name>
|
||||
|
||||
<properties>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
<jasperreports7.version>7.0.3</jasperreports7.version>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>net.sf.jasperreports</groupId>
|
||||
<artifactId>jasperreports</artifactId>
|
||||
<version>${jasperreports7.version}</version>
|
||||
<exclusions>
|
||||
<!-- not necessary to compile and it force dependency convergence issues -->
|
||||
<exclusion>
|
||||
<groupId>com.fasterxml.jackson.core</groupId>
|
||||
<artifactId>jackson-databind</artifactId>
|
||||
</exclusion>
|
||||
<exclusion>
|
||||
<groupId>com.fasterxml.jackson.core</groupId>
|
||||
<artifactId>jackson-annotations</artifactId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>net.sf.jasperreports</groupId>
|
||||
<artifactId>jasperreports-pdf</artifactId>
|
||||
<version>${jasperreports7.version}</version>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.fasterxml.jackson.core</groupId>
|
||||
<artifactId>jackson-databind</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.struts</groupId>
|
||||
<artifactId>struts2-junit-plugin</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework</groupId>
|
||||
<artifactId>spring-web</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.easymock</groupId>
|
||||
<artifactId>easymock</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
+65
@@ -0,0 +1,65 @@
|
||||
/*
|
||||
* 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.views.jasperreports7;
|
||||
|
||||
import net.sf.jasperreports.engine.JasperReport;
|
||||
import org.apache.struts2.ActionInvocation;
|
||||
import org.apache.struts2.StrutsException;
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
public interface JasperReport7Aware {
|
||||
|
||||
/**
|
||||
* Used to perform an action before report is going to be generated
|
||||
*
|
||||
* @param invocation current {@link ActionInvocation}
|
||||
*/
|
||||
default void beforeReportGeneration(ActionInvocation invocation) throws StrutsException {
|
||||
}
|
||||
|
||||
/**
|
||||
* Used to perform an action before report is going to be generated
|
||||
*
|
||||
* @param invocation current {@link ActionInvocation}
|
||||
*/
|
||||
default void afterReportGeneration(ActionInvocation invocation, JasperReport jasperReport) throws StrutsException {
|
||||
}
|
||||
|
||||
/**
|
||||
* Allows to specify action specific CSV delimiter, if returns null,
|
||||
* default one specified by {@link JasperReport7Constants#STRUTS_JASPER_REPORT_CSV_DELIMITER} will be used
|
||||
*
|
||||
* @return delimiter or null
|
||||
*/
|
||||
default String getCsvDelimiter(ActionInvocation invocation) {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Allows to specify different local than used by the framework or an action
|
||||
*
|
||||
* @param invocation current {@link ActionInvocation}
|
||||
* @return locale or null
|
||||
*/
|
||||
default Locale getReportLocale(ActionInvocation invocation) {
|
||||
return invocation.getInvocationContext().getLocale();
|
||||
}
|
||||
|
||||
}
|
||||
+63
@@ -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.views.jasperreports7;
|
||||
|
||||
public interface JasperReport7Constants {
|
||||
|
||||
/**
|
||||
* PDF format constant
|
||||
*/
|
||||
String FORMAT_PDF = "pdf";
|
||||
|
||||
/**
|
||||
* XML format constant
|
||||
*/
|
||||
String FORMAT_XML = "xml";
|
||||
|
||||
/**
|
||||
* HTML format constant
|
||||
*/
|
||||
String FORMAT_HTML = "html";
|
||||
|
||||
/**
|
||||
* XLSX format constant
|
||||
*/
|
||||
String FORMAT_XLSX = "xlsx";
|
||||
|
||||
/**
|
||||
* CSV format constant
|
||||
*/
|
||||
String FORMAT_CSV = "csv";
|
||||
|
||||
/**
|
||||
* RTF format constant
|
||||
*/
|
||||
String FORMAT_RTF = "rtf";
|
||||
|
||||
/**
|
||||
* Allows to define a custom default delimiter when exporting report into CSV file
|
||||
*/
|
||||
String STRUTS_JASPER_REPORT_CSV_DELIMITER = "struts.jasperReport7.csv.defaultDelimiter";
|
||||
|
||||
/**
|
||||
* Allows to define a custom url to image servlet used when exporting report into HTML
|
||||
*/
|
||||
String STRUTS_JASPER_REPORT_HTML_IMAGE_SERVLET_URL = "struts.jasperReport7.html.imageServletUrl";
|
||||
|
||||
}
|
||||
+384
@@ -0,0 +1,384 @@
|
||||
/*
|
||||
* 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.views.jasperreports7;
|
||||
|
||||
import jakarta.servlet.ServletContext;
|
||||
import jakarta.servlet.ServletException;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import net.sf.jasperreports.engine.JRException;
|
||||
import net.sf.jasperreports.engine.JRParameter;
|
||||
import net.sf.jasperreports.engine.JasperFillManager;
|
||||
import net.sf.jasperreports.engine.JasperPrint;
|
||||
import net.sf.jasperreports.engine.JasperReport;
|
||||
import net.sf.jasperreports.engine.util.JRLoader;
|
||||
import net.sf.jasperreports.export.Exporter;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.apache.struts2.ActionInvocation;
|
||||
import org.apache.struts2.StrutsException;
|
||||
import org.apache.struts2.inject.Inject;
|
||||
import org.apache.struts2.result.StrutsResultSupport;
|
||||
import org.apache.struts2.security.NotExcludedAcceptedPatternsChecker;
|
||||
import org.apache.struts2.util.ValueStack;
|
||||
import org.apache.struts2.views.jasperreports7.export.JasperReport7ExporterProvider;
|
||||
|
||||
import java.io.File;
|
||||
import java.sql.Connection;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.TimeZone;
|
||||
|
||||
/**
|
||||
* <!-- START SNIPPET: description -->
|
||||
* <p>
|
||||
* Generates a JasperReports report using the specified format or PDF if no
|
||||
* format is specified.
|
||||
* </p>
|
||||
* <!-- END SNIPPET: description -->
|
||||
* <p>
|
||||
* <b>This result type takes the following parameters:</b>
|
||||
* </p>
|
||||
* <!-- START SNIPPET: params -->
|
||||
*
|
||||
* <ul>
|
||||
*
|
||||
* <li><b>location (default)</b> - the location where the compiled jasper report
|
||||
* definition is (foo.jasper), relative from current URL.</li>
|
||||
* <li><b>dataSource (required)</b> - the EL expression used to retrieve the
|
||||
* datasource from the value stack (usually a List).</li>
|
||||
* <li><b>parse</b> - true by default. If set to false, all the parameters will
|
||||
* not be parsed for EL expressions.</li>
|
||||
* <li><b>format</b> - the format in which the report should be generated. Valid
|
||||
* values can be found in {@link JasperReport7Constants}. If no format is
|
||||
* specified, PDF will be used.</li>
|
||||
* <li><b>contentDisposition</b> - disposition (defaults to "inline", values are
|
||||
* typically <i>filename="document.pdf"</i>).</li>
|
||||
* <li><b>documentName</b> - name of the document (will generate the http header
|
||||
* <code>Content-disposition = X; filename=X.[format]</code>).</li>
|
||||
* <li>
|
||||
* <b>reportParameters</b> - an expression used to retrieve a map of
|
||||
* report parameters from the value stack. The parameters may be accessed
|
||||
* in the report via the usual JR mechanism and might include data not
|
||||
* part of the dataSource, such as the user name of the report creator, etc.
|
||||
* </li>
|
||||
* <li>
|
||||
* <b>connection</b> - a JDBC Connection which can be passed to the
|
||||
* report instead of dataSource
|
||||
* </li>
|
||||
* <li><b>wrapField</b> - defines if fields should warp with ValueStackDataSource
|
||||
* see <a href="https://issues.apache.org/jira/browse/WW-3698">WW-3698</a> for more details
|
||||
* </li>
|
||||
* </ul>
|
||||
* <p>
|
||||
* This result follows the same rules from {@link StrutsResultSupport}.
|
||||
* Specifically, all parameters will be parsed if the "parse" parameter
|
||||
* is not set to false.
|
||||
* </p>
|
||||
* <!-- END SNIPPET: params -->
|
||||
* <p><b>Example:</b></p>
|
||||
* <pre>
|
||||
* <!-- START SNIPPET: example1 -->
|
||||
* <result name="success" type="jasperReport7">
|
||||
* <param name="location">foo.jasper</param>
|
||||
* <param name="dataSource">mySource</param>
|
||||
* <param name="format">CSV</param>
|
||||
* </result>
|
||||
* <!-- END SNIPPET: example1 -->
|
||||
* </pre>
|
||||
* <p>
|
||||
* or for pdf
|
||||
*
|
||||
* <pre>
|
||||
* <!-- START SNIPPET: example2 -->
|
||||
* <result name="success" type="jasperReport7">
|
||||
* <param name="location">foo.jasper</param>
|
||||
* <param name="dataSource">mySource</param>
|
||||
* </result>
|
||||
* <!-- END SNIPPET: example2 -->
|
||||
* </pre>
|
||||
*/
|
||||
public class JasperReport7Result extends StrutsResultSupport implements JasperReport7Constants {
|
||||
|
||||
private static final Logger LOG = LogManager.getLogger(JasperReport7Result.class);
|
||||
|
||||
private String parsedDataSource;
|
||||
|
||||
protected String dataSource;
|
||||
protected String format;
|
||||
protected String documentName;
|
||||
protected String contentDisposition;
|
||||
protected String timeZone;
|
||||
|
||||
protected boolean wrapField = true;
|
||||
|
||||
/**
|
||||
* Connection can be passed to the report instead of dataSource.
|
||||
*/
|
||||
protected String connection;
|
||||
|
||||
/**
|
||||
* Names a report parameters map stack value, allowing additional report parameters from the action.
|
||||
*/
|
||||
protected String reportParameters;
|
||||
private String parsedReportParameters;
|
||||
|
||||
/**
|
||||
* Parameters validator, excludes not accepted params
|
||||
*/
|
||||
private NotExcludedAcceptedPatternsChecker notExcludedAcceptedPatterns;
|
||||
|
||||
public JasperReport7Result() {
|
||||
super();
|
||||
}
|
||||
|
||||
@Inject
|
||||
public void setNotExcludedAcceptedPatterns(NotExcludedAcceptedPatternsChecker notExcludedAcceptedPatterns) {
|
||||
this.notExcludedAcceptedPatterns = notExcludedAcceptedPatterns;
|
||||
}
|
||||
|
||||
protected void doExecute(String finalLocation, ActionInvocation invocation) throws Exception {
|
||||
initializeProperties(invocation);
|
||||
|
||||
LOG.debug("Creating JasperReport for dataSource: {} and format: {}", dataSource, format);
|
||||
// Construct the data source for the report.
|
||||
ValueStack stack = invocation.getStack();
|
||||
Connection reportConnection = (Connection) stack.findValue(connection);
|
||||
ValueStackDataSource reportDataSource = null;
|
||||
if (reportConnection == null) {
|
||||
reportDataSource = prepareDataSource(stack);
|
||||
}
|
||||
|
||||
if (invocation.getAction() instanceof JasperReport7Aware action) {
|
||||
LOG.debug("Passing control to action: {} before generating report", invocation.getInvocationContext().getActionName());
|
||||
action.beforeReportGeneration(invocation);
|
||||
}
|
||||
|
||||
ServletContext servletContext = invocation.getInvocationContext().getServletContext();
|
||||
String systemId = servletContext.getRealPath(finalLocation);
|
||||
Map<String, Object> parameters = new ValueStackShadowMap(stack);
|
||||
File directory = new File(systemId.substring(0, systemId.lastIndexOf(File.separator)));
|
||||
parameters.put("reportDirectory", directory);
|
||||
|
||||
applyLocale(invocation, parameters);
|
||||
applyTimeZone(invocation, parameters);
|
||||
applyCustomParameters(stack, parameters);
|
||||
|
||||
JasperPrint jasperPrint;
|
||||
|
||||
// Fill the report and produce a print object
|
||||
try {
|
||||
JasperReport jasperReport = (JasperReport) JRLoader.loadObject(new File(systemId));
|
||||
if (reportConnection == null) {
|
||||
jasperPrint = JasperFillManager.fillReport(jasperReport, parameters, reportDataSource);
|
||||
} else {
|
||||
jasperPrint = JasperFillManager.fillReport(jasperReport, parameters, reportConnection);
|
||||
}
|
||||
|
||||
if (invocation.getAction() instanceof JasperReport7Aware action) {
|
||||
LOG.debug("Passing control to action: {} after generating report: {}",
|
||||
invocation.getInvocationContext().getActionName(), jasperReport.getName());
|
||||
action.afterReportGeneration(invocation, jasperReport);
|
||||
}
|
||||
} catch (JRException e) {
|
||||
LOG.error("Error building report for uri: {}", systemId, e);
|
||||
throw new ServletException(e.getMessage(), e);
|
||||
}
|
||||
|
||||
try {
|
||||
LOG.debug("Export the print object to the desired output format: {}", format);
|
||||
JasperReport7ExporterProvider<?> exporterProvider = invocation.getInvocationContext().getContainer().getInstance(JasperReport7ExporterProvider.class, format);
|
||||
if (exporterProvider == null) {
|
||||
throw new StrutsException("No exporter found for format: " + format);
|
||||
}
|
||||
exportReport(invocation, jasperPrint, exporterProvider);
|
||||
} catch (StrutsException e) {
|
||||
LOG.error("Error producing: {} report for uri: {}", format, systemId, e);
|
||||
throw new ServletException(e.getMessage(), e);
|
||||
} finally {
|
||||
try {
|
||||
if (reportConnection != null) {
|
||||
reportConnection.close();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
LOG.warn("Could not close db connection properly", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected ValueStackDataSource prepareDataSource(ValueStack stack) throws ServletException {
|
||||
boolean evaluated = parsedDataSource != null && !parsedDataSource.equals(dataSource);
|
||||
boolean reevaluate = !evaluated || isAcceptableExpression(parsedDataSource);
|
||||
if (reevaluate) {
|
||||
return new ValueStackDataSource(stack, parsedDataSource, wrapField);
|
||||
} else {
|
||||
throw new ServletException(String.format("Unaccepted dataSource expression [%s]", parsedDataSource));
|
||||
}
|
||||
}
|
||||
|
||||
protected void applyLocale(ActionInvocation invocation, Map<String, Object> parameters) {
|
||||
Locale locale = null;
|
||||
if (invocation.getAction() instanceof JasperReport7Aware action) {
|
||||
locale = action.getReportLocale(invocation);
|
||||
}
|
||||
if (locale == null) {
|
||||
locale = invocation.getInvocationContext().getLocale();
|
||||
}
|
||||
|
||||
LOG.debug("Using locale: {} to generate report", locale);
|
||||
parameters.put(JRParameter.REPORT_LOCALE, locale);
|
||||
}
|
||||
|
||||
protected void applyTimeZone(ActionInvocation invocation, Map<String, Object> parameters) {
|
||||
if (timeZone != null) {
|
||||
timeZone = conditionalParse(timeZone, invocation);
|
||||
LOG.debug("Puts timezone in jasper report parameter: {}", timeZone);
|
||||
final TimeZone tz = TimeZone.getTimeZone(timeZone);
|
||||
if (tz != null) {
|
||||
parameters.put(JRParameter.REPORT_TIME_ZONE, tz);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
protected void applyCustomParameters(ValueStack stack, Map<String, Object> parameters) {
|
||||
boolean evaluated = parsedReportParameters != null && !parsedReportParameters.equals(reportParameters);
|
||||
boolean reevaluate = !evaluated || isAcceptableExpression(parsedReportParameters);
|
||||
Map<String, Object> reportParams = reevaluate ? (Map<String, Object>) stack.findValue(parsedReportParameters) : null;
|
||||
if (reportParams != null) {
|
||||
LOG.debug("Found report parameters: {}", reportParams);
|
||||
parameters.putAll(reportParams);
|
||||
}
|
||||
}
|
||||
|
||||
protected void exportReport(ActionInvocation invocation, JasperPrint jasperPrint, JasperReport7ExporterProvider<?> exporterProvider) throws StrutsException {
|
||||
HttpServletResponse response = prepapreHttpServletResponse(invocation);
|
||||
try {
|
||||
Exporter<?, ?, ?, ?> exporter = exporterProvider.createExporter(invocation, jasperPrint);
|
||||
|
||||
LOG.debug("Exporting report: {} as: {} and flushing response stream", jasperPrint.getName(), format);
|
||||
exporter.exportReport();
|
||||
response.getOutputStream().flush();
|
||||
} catch (Exception e) {
|
||||
throw new StrutsException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private HttpServletResponse prepapreHttpServletResponse(ActionInvocation invocation) {
|
||||
HttpServletResponse response = invocation.getInvocationContext().getServletResponse();
|
||||
|
||||
if (contentDisposition != null || documentName != null) {
|
||||
final StringBuilder tmp = new StringBuilder();
|
||||
tmp.append((contentDisposition == null) ? "inline" : contentDisposition);
|
||||
|
||||
if (documentName != null) {
|
||||
tmp.append("; filename=");
|
||||
tmp.append(documentName);
|
||||
tmp.append(".");
|
||||
tmp.append(format);
|
||||
}
|
||||
|
||||
response.setHeader("Content-disposition", tmp.toString());
|
||||
}
|
||||
return response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up result properties, parsing etc.
|
||||
*
|
||||
* @param invocation Current invocation.
|
||||
*/
|
||||
private void initializeProperties(ActionInvocation invocation) {
|
||||
if (dataSource == null && connection == null) {
|
||||
String message = "No dataSource specified...";
|
||||
LOG.error(message);
|
||||
throw new RuntimeException(message);
|
||||
}
|
||||
if (dataSource != null) {
|
||||
parsedDataSource = conditionalParse(dataSource, invocation);
|
||||
}
|
||||
|
||||
format = conditionalParse(format, invocation);
|
||||
if (StringUtils.isEmpty(format)) {
|
||||
format = FORMAT_PDF;
|
||||
}
|
||||
|
||||
if (contentDisposition != null) {
|
||||
contentDisposition = conditionalParse(contentDisposition, invocation);
|
||||
}
|
||||
|
||||
if (documentName != null) {
|
||||
documentName = conditionalParse(documentName, invocation);
|
||||
}
|
||||
|
||||
parsedReportParameters = conditionalParse(reportParameters, invocation);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if expression doesn't contain vulnerable code
|
||||
*
|
||||
* @param expression of result
|
||||
* @return true|false
|
||||
* @since 6.0.0
|
||||
*/
|
||||
protected boolean isAcceptableExpression(String expression) {
|
||||
NotExcludedAcceptedPatternsChecker.IsAllowed isAllowed = notExcludedAcceptedPatterns.isAllowed(expression);
|
||||
if (isAllowed.isAllowed()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
LOG.warn("Expression [{}] isn't allowed by pattern [{}]! See Accepted / Excluded patterns at\n" +
|
||||
"https://struts.apache.org/security/", expression, isAllowed.getAllowedPattern());
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public void setDataSource(String dataSource) {
|
||||
this.dataSource = dataSource;
|
||||
}
|
||||
|
||||
public void setFormat(String format) {
|
||||
this.format = format;
|
||||
}
|
||||
|
||||
public void setDocumentName(String documentName) {
|
||||
this.documentName = documentName;
|
||||
}
|
||||
|
||||
public void setContentDisposition(String contentDisposition) {
|
||||
this.contentDisposition = contentDisposition;
|
||||
}
|
||||
|
||||
public void setTimeZone(final String timeZone) {
|
||||
this.timeZone = timeZone;
|
||||
}
|
||||
|
||||
public void setWrapField(boolean wrapField) {
|
||||
this.wrapField = wrapField;
|
||||
}
|
||||
|
||||
public void setReportParameters(String reportParameters) {
|
||||
this.reportParameters = reportParameters;
|
||||
}
|
||||
|
||||
public void setConnection(String connection) {
|
||||
this.connection = connection;
|
||||
}
|
||||
|
||||
}
|
||||
+145
@@ -0,0 +1,145 @@
|
||||
/*
|
||||
* 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.views.jasperreports7;
|
||||
|
||||
import net.sf.jasperreports.engine.JRException;
|
||||
import net.sf.jasperreports.engine.JRField;
|
||||
import net.sf.jasperreports.engine.JRRewindableDataSource;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.apache.struts2.util.MakeIterator;
|
||||
import org.apache.struts2.util.ValueStack;
|
||||
|
||||
import java.util.Iterator;
|
||||
|
||||
/**
|
||||
* Ported to Struts.
|
||||
*/
|
||||
public class ValueStackDataSource implements JRRewindableDataSource {
|
||||
|
||||
private static final Logger LOG = LogManager.getLogger(ValueStackDataSource.class);
|
||||
|
||||
private final ValueStack valueStack;
|
||||
private final String dataSource;
|
||||
private final boolean wrapField;
|
||||
|
||||
private Iterator<?> iterator;
|
||||
|
||||
private boolean firstTimeThrough = true;
|
||||
|
||||
/**
|
||||
* Create a value stack data source on the given iterable property
|
||||
*
|
||||
* @param valueStack The value stack to base the data source on
|
||||
* @param dataSourceParam The property to iterate over for the report
|
||||
*/
|
||||
public ValueStackDataSource(ValueStack valueStack, String dataSourceParam, boolean wrapField) {
|
||||
this.valueStack = valueStack;
|
||||
this.dataSource = dataSourceParam;
|
||||
this.wrapField = wrapField;
|
||||
|
||||
Object dataSourceValue = valueStack.findValue(dataSource);
|
||||
|
||||
if (dataSourceValue != null) {
|
||||
if (MakeIterator.isIterable(dataSourceValue)) {
|
||||
iterator = MakeIterator.convert(dataSourceValue);
|
||||
} else {
|
||||
Object[] array = new Object[1];
|
||||
array[0] = dataSourceValue;
|
||||
iterator = MakeIterator.convert(array);
|
||||
}
|
||||
} else {
|
||||
LOG.warn("Data source value for data source: {} was null", dataSource);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get the value of a given field
|
||||
*
|
||||
* @param field The field to get the value for. The expression language to get the value
|
||||
* of the field is either taken from the description property or from the name of the field
|
||||
* if the description is <code>null</code>.
|
||||
* @return an <code>Object</code> containing the field value or a new
|
||||
* <code>ValueStackDataSource</code> object if the field value evaluates to
|
||||
* an object that can be iterated over.
|
||||
*/
|
||||
public Object getFieldValue(JRField field) {
|
||||
String expression = field.getName();
|
||||
|
||||
Object value = valueStack.findValue(expression);
|
||||
LOG.debug("Field [{}] = [{}]", field.getName(), value);
|
||||
|
||||
if (!wrapField && MakeIterator.isIterable(value) && field.getValueClass().isInstance(value)) {
|
||||
return value;
|
||||
} else if (MakeIterator.isIterable(value)) {
|
||||
// wrap value with ValueStackDataSource if not already wrapped
|
||||
return new ValueStackDataSource(this.valueStack, expression, wrapField);
|
||||
} else {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Move to the first item.
|
||||
*/
|
||||
public void moveFirst() {
|
||||
Object dataSourceValue = valueStack.findValue(dataSource);
|
||||
if (dataSourceValue != null) {
|
||||
if (MakeIterator.isIterable(dataSourceValue)) {
|
||||
iterator = MakeIterator.convert(dataSourceValue);
|
||||
} else {
|
||||
Object[] array = new Object[1];
|
||||
array[0] = dataSourceValue;
|
||||
iterator = MakeIterator.convert(array);
|
||||
}
|
||||
} else {
|
||||
LOG.warn("Data source value for data source [{}] was null", dataSource);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Is there any more data
|
||||
*
|
||||
* @return <code>true</code> if there are more elements to iterate over and
|
||||
* <code>false</code> otherwise
|
||||
* @throws JRException if there is a problem determining whether there
|
||||
* is more data
|
||||
*/
|
||||
public boolean next() throws JRException {
|
||||
if (firstTimeThrough) {
|
||||
firstTimeThrough = false;
|
||||
} else {
|
||||
valueStack.pop();
|
||||
}
|
||||
|
||||
if ((iterator != null) && (iterator.hasNext())) {
|
||||
valueStack.push(iterator.next());
|
||||
if (LOG.isDebugEnabled()) {
|
||||
LOG.debug("Pushed next value: {}", valueStack.findValue("."));
|
||||
}
|
||||
|
||||
return true;
|
||||
} else {
|
||||
LOG.debug("No more values");
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
+82
@@ -0,0 +1,82 @@
|
||||
/*
|
||||
* 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.views.jasperreports7;
|
||||
|
||||
import org.apache.struts2.util.ValueStack;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Objects;
|
||||
|
||||
|
||||
/**
|
||||
* Ported to Struts:
|
||||
*/
|
||||
public class ValueStackShadowMap extends HashMap<String, Object> {
|
||||
|
||||
/**
|
||||
* valueStack reference
|
||||
*/
|
||||
transient ValueStack valueStack;
|
||||
|
||||
/**
|
||||
* Constructs an instance of ValueStackShadowMap.
|
||||
*
|
||||
* @param valueStack - the underlying valuestack
|
||||
*/
|
||||
public ValueStackShadowMap(ValueStack valueStack) {
|
||||
this.valueStack = valueStack;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Implementation of containsKey(), overriding HashMap implementation.
|
||||
*
|
||||
* @param key - The key to check in HashMap and if not found to check on valueStack.
|
||||
* @return <tt>true</tt>, if contains key, <tt>false</tt> otherwise.
|
||||
* @see java.util.HashMap#containsKey
|
||||
*/
|
||||
@Override
|
||||
public boolean containsKey(Object key) {
|
||||
boolean hasKey = super.containsKey(key);
|
||||
|
||||
if (!hasKey && key != null && valueStack.findValue(key.toString()) != null) {
|
||||
hasKey = true;
|
||||
}
|
||||
|
||||
return hasKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* Implementation of get(), overriding HashMap implementation.
|
||||
*
|
||||
* @param key - The key to get in HashMap and if not found there from the valueStack.
|
||||
* @return value - The object from HashMap or if null, from the valueStack.
|
||||
* @see java.util.HashMap#get
|
||||
*/
|
||||
@Override
|
||||
public Object get(Object key) {
|
||||
Object value = super.get(key);
|
||||
|
||||
if (key != null && value == null) {
|
||||
value = valueStack.findValue(key.toString());
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
}
|
||||
+96
@@ -0,0 +1,96 @@
|
||||
/*
|
||||
* 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.views.jasperreports7.export;
|
||||
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import net.sf.jasperreports.engine.JasperPrint;
|
||||
import net.sf.jasperreports.engine.export.JRCsvExporter;
|
||||
import net.sf.jasperreports.export.SimpleCsvExporterConfiguration;
|
||||
import net.sf.jasperreports.export.SimpleExporterInput;
|
||||
import net.sf.jasperreports.export.SimpleWriterExporterOutput;
|
||||
import net.sf.jasperreports.export.WriterExporterOutput;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.apache.struts2.ActionInvocation;
|
||||
import org.apache.struts2.StrutsException;
|
||||
import org.apache.struts2.inject.Inject;
|
||||
import org.apache.struts2.views.jasperreports7.JasperReport7Aware;
|
||||
import org.apache.struts2.views.jasperreports7.JasperReport7Constants;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
|
||||
public class JasperReport7CsvExporterProvider implements JasperReport7ExporterProvider<JRCsvExporter> {
|
||||
|
||||
private static final Logger LOG = LogManager.getLogger(JasperReport7CsvExporterProvider.class);
|
||||
|
||||
/**
|
||||
* A delimiter used when generating CSV report. By default, "," is used.
|
||||
*/
|
||||
private String defaultDelimiter = ",";
|
||||
|
||||
@Inject
|
||||
public JasperReport7CsvExporterProvider(
|
||||
@Inject(value = JasperReport7Constants.STRUTS_JASPER_REPORT_CSV_DELIMITER, required = false)
|
||||
String defaultDelimiter
|
||||
) {
|
||||
if (StringUtils.isNoneEmpty(defaultDelimiter)) {
|
||||
LOG.debug("Using custom default delimiter [{}]", defaultDelimiter);
|
||||
this.defaultDelimiter = defaultDelimiter;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public JRCsvExporter createExporter(ActionInvocation invocation, JasperPrint jasperPrint) throws StrutsException {
|
||||
LOG.debug("Creating: {} exporter", this.getClass().getSimpleName());
|
||||
|
||||
HttpServletResponse response = invocation.getInvocationContext().getServletResponse();
|
||||
response.setContentType("text/csv");
|
||||
|
||||
JRCsvExporter exporter = new JRCsvExporter();
|
||||
|
||||
String reportDelimiter = null;
|
||||
if (invocation.getAction() instanceof JasperReport7Aware action) {
|
||||
reportDelimiter = action.getCsvDelimiter(invocation);
|
||||
}
|
||||
if (StringUtils.isEmpty(reportDelimiter)) {
|
||||
reportDelimiter = defaultDelimiter;
|
||||
}
|
||||
LOG.debug("Using delimiter: [{}]", reportDelimiter);
|
||||
|
||||
SimpleCsvExporterConfiguration config = new SimpleCsvExporterConfiguration();
|
||||
config.setFieldDelimiter(reportDelimiter);
|
||||
config.setRecordDelimiter(reportDelimiter);
|
||||
exporter.setConfiguration(config);
|
||||
|
||||
SimpleExporterInput input = new SimpleExporterInput(jasperPrint);
|
||||
exporter.setExporterInput(input);
|
||||
|
||||
try (OutputStream responseStream = response.getOutputStream()) {
|
||||
WriterExporterOutput exporterOutput = new SimpleWriterExporterOutput(responseStream);
|
||||
exporter.setExporterOutput(exporterOutput);
|
||||
} catch (IOException e) {
|
||||
LOG.error("Error writing CSV report output using: {}", JasperReport7CsvExporterProvider.class.getName(), e);
|
||||
throw new StrutsException(e.getMessage(), e);
|
||||
}
|
||||
|
||||
return exporter;
|
||||
}
|
||||
}
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
* 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.views.jasperreports7.export;
|
||||
|
||||
import net.sf.jasperreports.engine.JRAbstractExporter;
|
||||
import net.sf.jasperreports.engine.JasperPrint;
|
||||
import net.sf.jasperreports.engine.export.JRExporterContext;
|
||||
import net.sf.jasperreports.export.ExporterConfiguration;
|
||||
import net.sf.jasperreports.export.ExporterOutput;
|
||||
import net.sf.jasperreports.export.ReportExportConfiguration;
|
||||
import org.apache.struts2.ActionInvocation;
|
||||
import org.apache.struts2.StrutsException;
|
||||
|
||||
public interface JasperReport7ExporterProvider<T extends JRAbstractExporter<? extends ReportExportConfiguration, ? extends ExporterConfiguration, ? extends ExporterOutput, ? extends JRExporterContext>> {
|
||||
|
||||
T createExporter(ActionInvocation invocation, JasperPrint jasperPrint) throws StrutsException;
|
||||
|
||||
}
|
||||
+85
@@ -0,0 +1,85 @@
|
||||
/*
|
||||
* 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.views.jasperreports7.export;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import net.sf.jasperreports.engine.JasperPrint;
|
||||
import net.sf.jasperreports.engine.export.HtmlExporter;
|
||||
import net.sf.jasperreports.engine.export.HtmlResourceHandler;
|
||||
import net.sf.jasperreports.export.SimpleExporterInput;
|
||||
import net.sf.jasperreports.export.SimpleHtmlExporterOutput;
|
||||
import net.sf.jasperreports.web.util.WebHtmlResourceHandler;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.apache.struts2.ActionInvocation;
|
||||
import org.apache.struts2.StrutsException;
|
||||
import org.apache.struts2.inject.Inject;
|
||||
import org.apache.struts2.views.jasperreports7.JasperReport7Constants;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
|
||||
public class JasperReport7HtmlExporterProvider implements JasperReport7ExporterProvider<HtmlExporter> {
|
||||
|
||||
private static final Logger LOG = LogManager.getLogger(JasperReport7HtmlExporterProvider.class);
|
||||
|
||||
/**
|
||||
* Name of the url that, when prefixed with the context page, can return report images
|
||||
*/
|
||||
private String imageServletUrl = "/images/";
|
||||
|
||||
@Inject
|
||||
public JasperReport7HtmlExporterProvider(
|
||||
@Inject(value = JasperReport7Constants.STRUTS_JASPER_REPORT_HTML_IMAGE_SERVLET_URL, required = false)
|
||||
String imageServletUrl
|
||||
) {
|
||||
if (StringUtils.isNoneEmpty(imageServletUrl)) {
|
||||
LOG.debug("Using custom image servlet url: {}", imageServletUrl);
|
||||
this.imageServletUrl = imageServletUrl;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public HtmlExporter createExporter(ActionInvocation invocation, JasperPrint jasperPrint) throws StrutsException {
|
||||
LOG.debug("Creating: {} exporter with image servlet url: {}", this.getClass().getSimpleName(), imageServletUrl);
|
||||
|
||||
HttpServletRequest request = invocation.getInvocationContext().getServletRequest();
|
||||
HttpServletResponse response = invocation.getInvocationContext().getServletResponse();
|
||||
|
||||
response.setContentType("text/html");
|
||||
HtmlExporter exporter = new HtmlExporter();
|
||||
|
||||
SimpleExporterInput input = new SimpleExporterInput(jasperPrint);
|
||||
exporter.setExporterInput(input);
|
||||
|
||||
try (OutputStream responseStream = response.getOutputStream()) {
|
||||
SimpleHtmlExporterOutput exporterOutput = new SimpleHtmlExporterOutput(responseStream);
|
||||
HtmlResourceHandler imageHandler = new WebHtmlResourceHandler(request.getContextPath() + imageServletUrl + "%s");
|
||||
exporterOutput.setImageHandler(imageHandler);
|
||||
exporter.setExporterOutput(exporterOutput);
|
||||
} catch (IOException e) {
|
||||
LOG.error("Error writing HTML report output using: {}", JasperReport7HtmlExporterProvider.class.getName(), e);
|
||||
throw new StrutsException(e.getMessage(), e);
|
||||
}
|
||||
|
||||
return exporter;
|
||||
}
|
||||
}
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.struts2.views.jasperreports7.export;
|
||||
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import net.sf.jasperreports.engine.JasperPrint;
|
||||
import net.sf.jasperreports.export.OutputStreamExporterOutput;
|
||||
import net.sf.jasperreports.export.SimpleExporterInput;
|
||||
import net.sf.jasperreports.export.SimpleOutputStreamExporterOutput;
|
||||
import net.sf.jasperreports.pdf.JRPdfExporter;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.apache.struts2.ActionInvocation;
|
||||
import org.apache.struts2.StrutsException;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
|
||||
public class JasperReport7PdfExporterProvider implements JasperReport7ExporterProvider<JRPdfExporter> {
|
||||
|
||||
private static final Logger LOG = LogManager.getLogger(JasperReport7PdfExporterProvider.class);
|
||||
|
||||
@Override
|
||||
public JRPdfExporter createExporter(ActionInvocation invocation, JasperPrint jasperPrint) throws StrutsException {
|
||||
LOG.debug("Creating: {} exporter", this.getClass().getSimpleName());
|
||||
|
||||
HttpServletResponse response = invocation.getInvocationContext().getServletResponse();
|
||||
response.setContentType("application/pdf");
|
||||
|
||||
JRPdfExporter exporter = new JRPdfExporter();
|
||||
|
||||
SimpleExporterInput input = new SimpleExporterInput(jasperPrint);
|
||||
exporter.setExporterInput(input);
|
||||
|
||||
try (OutputStream responseStream = response.getOutputStream()) {
|
||||
OutputStreamExporterOutput exporterOutput = new SimpleOutputStreamExporterOutput(responseStream);
|
||||
exporter.setExporterOutput(exporterOutput);
|
||||
} catch (IOException e) {
|
||||
LOG.error("Error writing PDF report output using: {}", JasperReport7PdfExporterProvider.class.getName(), e);
|
||||
throw new StrutsException(e.getMessage(), e);
|
||||
}
|
||||
|
||||
return exporter;
|
||||
}
|
||||
}
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.struts2.views.jasperreports7.export;
|
||||
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import net.sf.jasperreports.engine.JasperPrint;
|
||||
import net.sf.jasperreports.engine.export.JRRtfExporter;
|
||||
import net.sf.jasperreports.export.SimpleExporterInput;
|
||||
import net.sf.jasperreports.export.SimpleWriterExporterOutput;
|
||||
import net.sf.jasperreports.export.WriterExporterOutput;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.apache.struts2.ActionInvocation;
|
||||
import org.apache.struts2.StrutsException;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
|
||||
public class JasperReport7RtfExporterProvider implements JasperReport7ExporterProvider<JRRtfExporter> {
|
||||
|
||||
private static final Logger LOG = LogManager.getLogger(JasperReport7RtfExporterProvider.class);
|
||||
|
||||
@Override
|
||||
public JRRtfExporter createExporter(ActionInvocation invocation, JasperPrint jasperPrint) throws StrutsException {
|
||||
LOG.debug("Creating: {} exporter", this.getClass().getSimpleName());
|
||||
|
||||
HttpServletResponse response = invocation.getInvocationContext().getServletResponse();
|
||||
response.setContentType("application/rtf");
|
||||
|
||||
JRRtfExporter exporter = new JRRtfExporter();
|
||||
|
||||
SimpleExporterInput input = new SimpleExporterInput(jasperPrint);
|
||||
exporter.setExporterInput(input);
|
||||
|
||||
try (OutputStream responseStream = response.getOutputStream()) {
|
||||
WriterExporterOutput exporterOutput = new SimpleWriterExporterOutput(responseStream);
|
||||
exporter.setExporterOutput(exporterOutput);
|
||||
} catch (IOException e) {
|
||||
LOG.error("Error writing RTF report output using: {}", JasperReport7RtfExporterProvider.class.getName(), e);
|
||||
throw new StrutsException(e.getMessage(), e);
|
||||
}
|
||||
|
||||
return exporter;
|
||||
}
|
||||
}
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.struts2.views.jasperreports7.export;
|
||||
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import net.sf.jasperreports.engine.JasperPrint;
|
||||
import net.sf.jasperreports.engine.export.ooxml.JRXlsxExporter;
|
||||
import net.sf.jasperreports.export.OutputStreamExporterOutput;
|
||||
import net.sf.jasperreports.export.SimpleExporterInput;
|
||||
import net.sf.jasperreports.export.SimpleOutputStreamExporterOutput;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.apache.struts2.ActionInvocation;
|
||||
import org.apache.struts2.StrutsException;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
|
||||
public class JasperReport7XlsxExporterProvider implements JasperReport7ExporterProvider<JRXlsxExporter> {
|
||||
|
||||
private static final Logger LOG = LogManager.getLogger(JasperReport7XlsxExporterProvider.class);
|
||||
|
||||
@Override
|
||||
public JRXlsxExporter createExporter(ActionInvocation invocation, JasperPrint jasperPrint) throws StrutsException {
|
||||
LOG.debug("Creating: {} exporter", this.getClass().getSimpleName());
|
||||
|
||||
HttpServletResponse response = invocation.getInvocationContext().getServletResponse();
|
||||
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
|
||||
|
||||
JRXlsxExporter exporter = new JRXlsxExporter();
|
||||
|
||||
SimpleExporterInput input = new SimpleExporterInput(jasperPrint);
|
||||
exporter.setExporterInput(input);
|
||||
|
||||
try (OutputStream responseStream = response.getOutputStream()) {
|
||||
OutputStreamExporterOutput exporterOutput = new SimpleOutputStreamExporterOutput(responseStream);
|
||||
exporter.setExporterOutput(exporterOutput);
|
||||
} catch (IOException e) {
|
||||
LOG.error("Error writing XLSX report output using: {}", JasperReport7XlsxExporterProvider.class.getName(), e);
|
||||
throw new StrutsException(e.getMessage(), e);
|
||||
}
|
||||
|
||||
return exporter;
|
||||
}
|
||||
}
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.struts2.views.jasperreports7.export;
|
||||
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import net.sf.jasperreports.engine.JasperPrint;
|
||||
import net.sf.jasperreports.engine.export.JRXmlExporter;
|
||||
import net.sf.jasperreports.export.SimpleExporterInput;
|
||||
import net.sf.jasperreports.export.SimpleXmlExporterOutput;
|
||||
import net.sf.jasperreports.export.XmlExporterOutput;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.apache.struts2.ActionInvocation;
|
||||
import org.apache.struts2.StrutsException;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
|
||||
public class JasperReport7XmlExporterProvider implements JasperReport7ExporterProvider<JRXmlExporter> {
|
||||
|
||||
private static final Logger LOG = LogManager.getLogger(JasperReport7XmlExporterProvider.class);
|
||||
|
||||
@Override
|
||||
public JRXmlExporter createExporter(ActionInvocation invocation, JasperPrint jasperPrint) throws StrutsException {
|
||||
LOG.debug("Creating: {} exporter", this.getClass().getSimpleName());
|
||||
|
||||
HttpServletResponse response = invocation.getInvocationContext().getServletResponse();
|
||||
response.setContentType("text/xml");
|
||||
|
||||
JRXmlExporter exporter = new JRXmlExporter();
|
||||
|
||||
SimpleExporterInput input = new SimpleExporterInput(jasperPrint);
|
||||
exporter.setExporterInput(input);
|
||||
|
||||
try (OutputStream responseOutput = response.getOutputStream()) {
|
||||
XmlExporterOutput exporterOutput = new SimpleXmlExporterOutput(responseOutput);
|
||||
exporter.setExporterOutput(exporterOutput);
|
||||
} catch (IOException e) {
|
||||
LOG.error("Error writing report XML output using: {}", JRXmlExporter.class.getName(), e);
|
||||
throw new StrutsException(e.getMessage(), e);
|
||||
}
|
||||
|
||||
return exporter;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,201 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
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.
|
||||
@@ -0,0 +1,5 @@
|
||||
Apache Struts
|
||||
Copyright 2000-2024 The Apache Software Foundation
|
||||
|
||||
This product includes software developed by
|
||||
The Apache Software Foundation (http://www.apache.org/).
|
||||
@@ -0,0 +1,59 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<!--
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
-->
|
||||
<!DOCTYPE struts PUBLIC
|
||||
"-//Apache Software Foundation//DTD Struts Configuration 6.0//EN"
|
||||
"https://struts.apache.org/dtds/struts-6.0.dtd">
|
||||
|
||||
<struts>
|
||||
<constant name="struts.jasperReport7.csv.defaultDelimiter" value=","/>
|
||||
|
||||
<bean name="pdf"
|
||||
class="org.apache.struts2.views.jasperreports7.export.JasperReport7PdfExporterProvider"
|
||||
type="org.apache.struts2.views.jasperreports7.export.JasperReport7ExporterProvider"/>
|
||||
|
||||
<bean name="csv"
|
||||
class="org.apache.struts2.views.jasperreports7.export.JasperReport7CsvExporterProvider"
|
||||
type="org.apache.struts2.views.jasperreports7.export.JasperReport7ExporterProvider"/>
|
||||
|
||||
<bean name="html"
|
||||
class="org.apache.struts2.views.jasperreports7.export.JasperReport7HtmlExporterProvider"
|
||||
type="org.apache.struts2.views.jasperreports7.export.JasperReport7ExporterProvider"/>
|
||||
|
||||
<bean name="xlsx"
|
||||
class="org.apache.struts2.views.jasperreports7.export.JasperReport7XlsxExporterProvider"
|
||||
type="org.apache.struts2.views.jasperreports7.export.JasperReport7ExporterProvider"/>
|
||||
|
||||
<bean name="xml"
|
||||
class="org.apache.struts2.views.jasperreports7.export.JasperReport7XmlExporterProvider"
|
||||
type="org.apache.struts2.views.jasperreports7.export.JasperReport7ExporterProvider"/>
|
||||
|
||||
<bean name="rtf"
|
||||
class="org.apache.struts2.views.jasperreports7.export.JasperReport7RtfExporterProvider"
|
||||
type="org.apache.struts2.views.jasperreports7.export.JasperReport7ExporterProvider"/>
|
||||
|
||||
<package name="jasperreports7-default" extends="struts-default">
|
||||
<result-types>
|
||||
<result-type name="jasperReport7" class="org.apache.struts2.views.jasperreports7.JasperReport7Result"/>
|
||||
</result-types>
|
||||
</package>
|
||||
|
||||
</struts>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user