Compare commits

...

63 Commits

Author SHA1 Message Date
Lukasz Lenart b039dc5079 WW-5326 fix(rat): adjust exclude patterns for RAT and adds missing header with license 2026-04-07 07:38:42 +02:00
Lukasz Lenart 6374e31384 WW-5326 fix(tiles): use raw OgnlContext in tiles accessors
Tiles accessors are called by OGNL internal machinery with raw OgnlContext,
not within a Struts evaluation context, so they cannot use StrutsContext.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-06 19:27:16 +02:00
Lukasz Lenart 803b5cbbd0 WW-5326 docs: add Jira reference to spec and plan
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-06 19:18:21 +02:00
Lukasz Lenart 1dda92ed23 WW-5326 test(ognl): update tests for StrutsContext migration
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-06 19:17:10 +02:00
Lukasz Lenart cda38c79db WW-5326 refactor(tiles): parameterize tiles OGNL accessors with StrutsContext
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-06 19:01:25 +02:00
Lukasz Lenart fbb904e9c6 WW-5326 refactor(ognl): replace Ognl.createDefaultContext with direct StrutsContext construction
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-06 18:41:27 +02:00
Lukasz Lenart f7923fc704 WW-5326 refactor(ognl): parameterize all accessor implementations with StrutsContext
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-06 18:31:58 +02:00
Lukasz Lenart 47718f5a8a WW-5326 refactor(ognl): parameterize core interfaces with StrutsContext
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-06 18:19:55 +02:00
Lukasz Lenart e2e8fa1f33 WW-5326 feat(ognl): introduce StrutsContext extending OgnlContext<StrutsContext>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-06 18:14:29 +02:00
Lukasz Lenart 71c204f04b WW-5326 build(deps): bump OGNL from 3.4.10 to 3.5.0-BETA4
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-06 18:10:50 +02:00
Lukasz Lenart d276b2dded WW-5622 perf(core): optimize Hibernate proxy detection when Hibernate is absent (#1649)
Detect Hibernate availability once at class-load time via Class.forName()
and short-circuit all Hibernate-related methods immediately when absent.
This eliminates repeated LinkageError/NoClassDefFoundError exceptions
that cause significant performance degradation in applications without
Hibernate on the classpath.

Fixes https://issues.apache.org/jira/browse/WW-5622

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-05 18:30:44 +02:00
Lukasz Lenart 620fcbd152 WW-5621 Harden XML parsers against Entity Expansion (Billion Laughs) attacks (#1642)
Modern JDKs (7u45+) already protect against this attack with a built-in
64K entity expansion limit. These changes add defense-in-depth hardening
and remove unnecessary attack surface.

- Remove unused parseStringAsXML feature from StringAdapter to eliminate
  a theoretical XML Entity Expansion vector
- Deprecate setParseStringAsXML() and getParseStringAsXML() for removal
- Enable SECURE_PROCESSING feature in DigesterDefinitionsReader
- Add unit test verifying JDK's entity expansion limit rejects
  Billion Laughs payloads
- Add research document with vulnerability analysis

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-04 11:20:33 +02:00
dependabot[bot] 41abbd1684 build(deps): bump org.apache.logging.log4j:log4j-bom (#1648)
Bumps [org.apache.logging.log4j:log4j-bom](https://github.com/apache/logging-log4j2) from 2.25.3 to 2.25.4.
- [Release notes](https://github.com/apache/logging-log4j2/releases)
- [Changelog](https://github.com/apache/logging-log4j2/blob/2.x/RELEASE-NOTES.adoc)
- [Commits](https://github.com/apache/logging-log4j2/compare/rel/2.25.3...rel/2.25.4)

---
updated-dependencies:
- dependency-name: org.apache.logging.log4j:log4j-bom
  dependency-version: 2.25.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-02 16:23:17 +02:00
dependabot[bot] b3ba3d04d5 build(deps-dev): bump byte-buddy.version from 1.18.7 to 1.18.8 (#1646)
Bumps `byte-buddy.version` from 1.18.7 to 1.18.8.

Updates `net.bytebuddy:byte-buddy` from 1.18.7 to 1.18.8
- [Release notes](https://github.com/raphw/byte-buddy/releases)
- [Changelog](https://github.com/raphw/byte-buddy/blob/master/release-notes.md)
- [Commits](https://github.com/raphw/byte-buddy/compare/byte-buddy-1.18.7...byte-buddy-1.18.8)

Updates `net.bytebuddy:byte-buddy-agent` from 1.18.7 to 1.18.8
- [Release notes](https://github.com/raphw/byte-buddy/releases)
- [Changelog](https://github.com/raphw/byte-buddy/blob/master/release-notes.md)
- [Commits](https://github.com/raphw/byte-buddy/compare/byte-buddy-1.18.7...byte-buddy-1.18.8)

---
updated-dependencies:
- dependency-name: net.bytebuddy:byte-buddy
  dependency-version: 1.18.8
  dependency-type: direct:development
  update-type: version-update:semver-patch
- dependency-name: net.bytebuddy:byte-buddy-agent
  dependency-version: 1.18.8
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-02 16:22:56 +02:00
dependabot[bot] c9918bf8bb build(deps): bump github/codeql-action from 4.34.1 to 4.35.1 (#1645)
Bumps [github/codeql-action](https://github.com/github/codeql-action) from 4.34.1 to 4.35.1.
- [Release notes](https://github.com/github/codeql-action/releases)
- [Commits](https://github.com/github/codeql-action/compare/v4.34.1...v4.35.1)

---
updated-dependencies:
- dependency-name: github/codeql-action
  dependency-version: 4.35.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-02 16:22:42 +02:00
Lukasz Lenart 2215b6873c WW-5537 Resolve classloader/memory leaks during Tomcat hot deployment (#1632)
* WW-5537 Add InternalDestroyable and ContextAwareDestroyable interfaces

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* WW-5537 ContainerHolder: ThreadLocal with AtomicLong generation counter

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* WW-5537 FinalizableReferenceQueue: volatile instance, join, classloader null

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* WW-5537 ScopeInterceptor.clearLocks: add synchronized block

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* WW-5537 CompoundRootAccessor, DefaultFileManager: implement InternalDestroyable

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* WW-5537 Add InternalDestroyable adapter classes for static cache cleanup

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* WW-5537 Register InternalDestroyable beans in struts-beans.xml

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* WW-5537 JSON plugin: add JSONCacheDestroyable for BeanInfo cache cleanup

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* WW-5537 Dispatcher.cleanup: refactor into focused methods with InternalDestroyable discovery

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* WW-5537 Rewrite DispatcherCleanupTest for InternalDestroyable discovery

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* WW-5537 Add log4j-web for proper Log4j2 lifecycle in Servlet container

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* WW-5537 Dispatcher.destroyObjectFactory: add early return on null, use pattern matching

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* WW-5537 Fix @since annotations: 7.1.0 -> 7.2.0

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* WW-5537 Add Container.destroy() to clear internal caches on undeploy

Container now exposes a destroy() method that clears factories, injectors,
constructors, and ThreadLocals. This releases Class<?> keys and JDK
DelegatingClassLoader instances that pin the webapp classloader.

DefaultConfiguration.destroy() calls container.destroy() and
reloadContainer() delegates to destroy() to avoid duplication.

Also fixes JSONCacheDestroyable referencing non-existent DefaultJSONWriter
(renamed to StrutsJSONWriter).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* WW-5537 Fix Container.destroy(): don't clear factories, don't call from reloadContainer

factories must remain intact because existing code holds direct
references to the Container after destroyConfiguration() and expects
it to still resolve dependencies (e.g. during configuration reload).

reloadContainer() reverted to clearing packageContexts/loadedFileNames
directly — calling destroy() there nulled the container reference and
cleared state needed during the bootstrap transition.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* WW-5537 Restore destroy() call in reloadContainer()

The test failures were caused by factories.clear() in
Container.destroy(), not by calling destroy() from reloadContainer().
Now that factories.clear() is removed, destroy() is safe to call
here — it clears packageContexts, loadedFileNames, and the container's
reflection caches in one place.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* WW-5537 Fix Sonar issues: thread-safe FinalizableReferenceQueue, empty method comments

- Replace volatile field with AtomicReference in FinalizableReferenceQueue
  for proper thread safety using getAndSet()
- Add comments to empty destroy() implementations in test mocks
- Replace deprecated new URL() with URI.toURL() in DispatcherCleanupTest
- Add comments to empty listener methods in DispatcherCleanupTest

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-29 07:17:57 +02:00
dependabot[bot] b0d6d4f2d4 build(deps): bump org.apache.maven.doxia:doxia-core from 2.0.0 to 2.1.0 (#1640)
Bumps [org.apache.maven.doxia:doxia-core](https://github.com/apache/maven-doxia) from 2.0.0 to 2.1.0.
- [Release notes](https://github.com/apache/maven-doxia/releases)
- [Commits](https://github.com/apache/maven-doxia/compare/doxia-2.0.0...doxia-2.1.0)

---
updated-dependencies:
- dependency-name: org.apache.maven.doxia:doxia-core
  dependency-version: 2.1.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-27 13:09:17 +01:00
dependabot[bot] da5908d0ea build(deps): bump org.apache.rat:apache-rat-plugin from 0.17 to 0.18 (#1639)
Bumps org.apache.rat:apache-rat-plugin from 0.17 to 0.18.

---
updated-dependencies:
- dependency-name: org.apache.rat:apache-rat-plugin
  dependency-version: '0.18'
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-27 13:09:00 +01:00
dependabot[bot] 20b20e47eb build(deps): bump com.fasterxml.jackson:jackson-bom (#1634)
Bumps [com.fasterxml.jackson:jackson-bom](https://github.com/FasterXML/jackson-bom) from 2.21.1 to 2.21.2.
- [Commits](https://github.com/FasterXML/jackson-bom/compare/jackson-bom-2.21.1...jackson-bom-2.21.2)

---
updated-dependencies:
- dependency-name: com.fasterxml.jackson:jackson-bom
  dependency-version: 2.21.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-27 13:06:42 +01:00
Lukasz Lenart f132a2e43b deps(rat): excludes updatding the RAT plugin as it requires Java 17 (#1641) 2026-03-27 10:40:00 +00:00
dependabot[bot] 556dece531 build(deps): bump github/codeql-action from 4.33.0 to 4.34.1 (#1633)
Bumps [github/codeql-action](https://github.com/github/codeql-action) from 4.33.0 to 4.34.1.
- [Release notes](https://github.com/github/codeql-action/releases)
- [Commits](https://github.com/github/codeql-action/compare/v4.33.0...v4.34.1)

---
updated-dependencies:
- dependency-name: github/codeql-action
  dependency-version: 4.34.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-27 11:32:18 +01:00
Lukasz Lenart 8ac63e535a WW-5618 feat(json): add configurable limits to JSON plugin (#1625)
Add configurable limits to the JSON plugin to prevent denial-of-service
attacks via malicious payloads (deeply nested objects, huge arrays, long
strings).

Changes:
- Extract JSONReader interface from class, create StrutsJSONReader impl
  with maxElements, maxDepth, maxStringLength, maxKeyLength enforcement
- Rename DefaultJSONWriter to StrutsJSONWriter (Struts* naming convention)
- Add JSONBeanSelectionProvider for bean aliasing via constants
- Update JSONUtil with @Inject for reader/writer, add instance
  deserializeInput() with maxLength check, deprecate static deserialize()
- Wire limits into JSONInterceptor with @Inject from constants
- Register beans and defaults in struts-plugin.xml

Default limits: 10K elements, 64 depth, 2MB length, 256KB strings, 512 keys.
All configurable via struts.xml constants or per-action interceptor params.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-21 12:11:06 +01:00
dependabot[bot] 311cda1681 build(deps): bump org.mockito:mockito-core from 5.22.0 to 5.23.0 (#1629)
Bumps [org.mockito:mockito-core](https://github.com/mockito/mockito) from 5.22.0 to 5.23.0.
- [Release notes](https://github.com/mockito/mockito/releases)
- [Commits](https://github.com/mockito/mockito/compare/v5.22.0...v5.23.0)

---
updated-dependencies:
- dependency-name: org.mockito:mockito-core
  dependency-version: 5.23.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-18 07:27:46 +01:00
dependabot[bot] 4f1e491ae8 build(deps): bump org.glassfish.jaxb:jaxb-bom from 4.0.6 to 4.0.7 (#1628)
Bumps org.glassfish.jaxb:jaxb-bom from 4.0.6 to 4.0.7.

---
updated-dependencies:
- dependency-name: org.glassfish.jaxb:jaxb-bom
  dependency-version: 4.0.7
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-18 07:27:34 +01:00
dependabot[bot] afa33e1b09 build(deps): bump github/codeql-action from 4.32.6 to 4.33.0 (#1627)
Bumps [github/codeql-action](https://github.com/github/codeql-action) from 4.32.6 to 4.33.0.
- [Release notes](https://github.com/github/codeql-action/releases)
- [Commits](https://github.com/github/codeql-action/compare/v4.32.6...v4.33.0)

---
updated-dependencies:
- dependency-name: github/codeql-action
  dependency-version: 4.33.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-18 07:27:16 +01:00
Lukasz Lenart 572f3aaf69 chore: harden showcase apps and convert READMEs to Markdown (#1624)
- Add production deployment warnings to showcase and rest-showcase READMEs
- Convert README.txt to README.md with proper Markdown formatting
- Restrict ViewSourceAction config parameter to XML files within webapp path

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-17 10:25:04 +01:00
Senrian 47d46f7013 WW-5617 Replace printStackTrace() with System.err in CompileReport (#1606)
Replace e.printStackTrace() with System.err.println() to properly
log errors to stderr without stack trace noise in CLI tools.

Issue: JRException handling in JasperReports compilation

Co-authored-by: Senrian <sen@senrian.com>
2026-03-13 16:32:12 +01:00
Lukasz Lenart 55e268b009 WW-2963 default-action-ref fails to find wildcard named actions (#1614)
* WW-2963 fix(core): resolve default-action-ref via wildcard matching

When default-action-ref names an action that only exists as a wildcard
pattern (e.g., "movie-list" matching "movie-*"), the fallback now tries
wildcard matching after the exact map lookup fails. This mirrors the
exact→wildcard resolution already used for request action names.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* WW-2963 refactor(core): reduce cognitive complexity of findActionConfigInNamespace

Extract default-action-ref resolution into findDefaultActionConfig() and
replace the deeply nested if-pyramid with early returns, reducing the
nesting depth from 5 to 1 to satisfy Sonar's complexity threshold.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
Made-with: Cursor

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-03-13 16:29:01 +01:00
dependabot[bot] ef00dd07a9 build(deps): bump net.sf.jasperreports:jasperreports (#1618)
Bumps [net.sf.jasperreports:jasperreports](https://github.com/Jaspersoft/jasperreports) from 7.0.3 to 7.0.4.
- [Release notes](https://github.com/Jaspersoft/jasperreports/releases)
- [Changelog](https://github.com/Jaspersoft/jasperreports/blob/master/changes.txt)
- [Commits](https://github.com/Jaspersoft/jasperreports/compare/7.0.3...7.0.4)

---
updated-dependencies:
- dependency-name: net.sf.jasperreports:jasperreports
  dependency-version: 7.0.4
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-11 13:30:42 +01:00
dependabot[bot] 26fede63ca build(deps-dev): bump commons-logging:commons-logging (#1620)
Bumps [commons-logging:commons-logging](https://github.com/apache/commons-logging) from 1.3.5 to 1.3.6.
- [Changelog](https://github.com/apache/commons-logging/blob/master/RELEASE-NOTES.txt)
- [Commits](https://github.com/apache/commons-logging/compare/rel/commons-logging-1.3.5...rel/commons-logging-1.3.6)

---
updated-dependencies:
- dependency-name: commons-logging:commons-logging
  dependency-version: 1.3.6
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-11 13:29:40 +01:00
dependabot[bot] b5d93f6860 build(deps): bump github/codeql-action from 4.32.5 to 4.32.6 (#1619)
Bumps [github/codeql-action](https://github.com/github/codeql-action) from 4.32.5 to 4.32.6.
- [Release notes](https://github.com/github/codeql-action/releases)
- [Commits](https://github.com/github/codeql-action/compare/v4.32.5...v4.32.6)

---
updated-dependencies:
- dependency-name: github/codeql-action
  dependency-version: 4.32.6
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-11 13:29:29 +01:00
Lukasz Lenart 944ad2f1e3 WW-4428 feat(json): add java.time serialization and deserialization support (#1603)
- Add serialization support for LocalDate, LocalDateTime, LocalTime,
  ZonedDateTime, OffsetDateTime, and Instant in DefaultJSONWriter
- Add deserialization support for the same types in JSONPopulator
- Support @JSON(format=...) custom formats for all temporal types
- Fix Instant custom-format serialization requiring UTC zone
- Add Calendar serialization/deserialization via temporal bridge
- Add comprehensive tests for all temporal types including custom
  formats, malformed input, and null handling

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 12:51:21 +01:00
Lukasz Lenart 8df3a4be13 Merge pull request #1613 from apache/chore/move-test-runner-agent-to-user-wide
chore: remove project-specific test-runner agent
2026-03-06 08:00:50 +01:00
Lukasz Lenart 121bb1ad22 chore: remove project-specific test-runner agent
Moved to user-wide ~/.claude/agents/ to make it available across all
projects. The agent is now project-agnostic and auto-detects build tools.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 07:56:36 +01:00
Lukasz Lenart 4c94c4f89a WW-5549 Fix I18nInterceptor supportedLocale breaking request_locale (#1594)
* fix(i18n): ensure request_locale takes precedence over Accept-Language when supportedLocale is configured

When supportedLocale was configured on the I18nInterceptor, the Accept-Language
header match in AcceptLanguageLocaleHandler.find() returned early before
SessionLocaleHandler/CookieLocaleHandler ever checked their explicit locale
parameters (request_locale, request_cookie_locale). This made it impossible
to switch locale via request parameters when supportedLocale was set.

Changes:
- Reorder AcceptLanguageLocaleHandler.find() to check request_only_locale
  before Accept-Language matching
- Reorder SessionLocaleHandler.find() to check request_locale before super
- Reorder CookieLocaleHandler.find() to check request_cookie_locale before super
- Add isLocaleSupported() helper to validate locales against supportedLocale
- Filter all locale sources (params, session, cookies) through supportedLocale
- Add 4 tests covering the bug scenario and supportedLocale filtering

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* test(i18n): cover missing supportedLocale locale-selection paths

Add regression tests for unsupported request_cookie_locale fallback, stored cookie revalidation, and request_only_locale precedence to lock in WW-5549 behavior across remaining branches.

Co-authored-by: Cursor <cursoragent@cursor.com>

* refactor(i18n): extract locale handlers with deprecated inner wrappers

Move locale handler implementations into a dedicated interceptor.i18n package with reusable abstract bases, keep thin deprecated inner wrappers in I18nInterceptor for one release-cycle compatibility, and document the LocaleHandler contract.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(i18n): validate request_only_locale against supportedLocale and fix Accept-Language fallback

RequestLocaleHandler.find() now checks isLocaleSupported() before
returning, preventing unsupported locales from slipping through via
the request_only_locale parameter. AcceptLanguageLocaleHandler.find()
now returns the first Accept-Language locale when supportedLocale is
empty, fixing ACCEPT_LANGUAGE storage mode with no filter configured.

Also includes refactoring: deprecated inner classes collapsed with
LocaleHandlerAdapter, shouldStore field encapsulated via disableStore(),
logger pattern standardized to private static final, and class-level
JavaDoc added to handler classes.

Made-with: Cursor

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-03-06 07:50:06 +01:00
dependabot[bot] 507c64e49a build(deps): bump org.apache.felix:maven-bundle-plugin (#1610)
Bumps org.apache.felix:maven-bundle-plugin from 6.0.0 to 6.0.2.

---
updated-dependencies:
- dependency-name: org.apache.felix:maven-bundle-plugin
  dependency-version: 6.0.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-04 06:48:32 +01:00
dependabot[bot] 246a2507bb build(deps): bump github/codeql-action from 4.32.4 to 4.32.5 (#1611)
Bumps [github/codeql-action](https://github.com/github/codeql-action) from 4.32.4 to 4.32.5.
- [Release notes](https://github.com/github/codeql-action/releases)
- [Commits](https://github.com/github/codeql-action/compare/v4.32.4...v4.32.5)

---
updated-dependencies:
- dependency-name: github/codeql-action
  dependency-version: 4.32.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-04 06:48:20 +01:00
dependabot[bot] 4af368720e build(deps): bump actions/upload-artifact from 6.0.0 to 7.0.0 (#1612)
Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 6.0.0 to 7.0.0.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/b7c566a772e6b6bfb58ed0dc250532a479d7789f...bbbca2ddaa5d8feaa63e36b76fdaad77386f024f)

---
updated-dependencies:
- dependency-name: actions/upload-artifact
  dependency-version: 7.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-04 06:48:11 +01:00
dependabot[bot] 28696ce1a7 build(deps): bump org.mockito:mockito-core from 5.21.0 to 5.22.0 (#1609)
Bumps [org.mockito:mockito-core](https://github.com/mockito/mockito) from 5.21.0 to 5.22.0.
- [Release notes](https://github.com/mockito/mockito/releases)
- [Commits](https://github.com/mockito/mockito/compare/v5.21.0...v5.22.0)

---
updated-dependencies:
- dependency-name: org.mockito:mockito-core
  dependency-version: 5.22.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-04 06:47:57 +01:00
dependabot[bot] 32a7789485 build(deps-dev): bump byte-buddy.version from 1.18.5 to 1.18.7 (#1607)
Bumps `byte-buddy.version` from 1.18.5 to 1.18.7.

Updates `net.bytebuddy:byte-buddy` from 1.18.5 to 1.18.7
- [Release notes](https://github.com/raphw/byte-buddy/releases)
- [Changelog](https://github.com/raphw/byte-buddy/blob/master/release-notes.md)
- [Commits](https://github.com/raphw/byte-buddy/compare/byte-buddy-1.18.5...byte-buddy-1.18.7)

Updates `net.bytebuddy:byte-buddy-agent` from 1.18.5 to 1.18.7
- [Release notes](https://github.com/raphw/byte-buddy/releases)
- [Changelog](https://github.com/raphw/byte-buddy/blob/master/release-notes.md)
- [Commits](https://github.com/raphw/byte-buddy/compare/byte-buddy-1.18.5...byte-buddy-1.18.7)

---
updated-dependencies:
- dependency-name: net.bytebuddy:byte-buddy
  dependency-version: 1.18.7
  dependency-type: direct:development
  update-type: version-update:semver-patch
- dependency-name: net.bytebuddy:byte-buddy-agent
  dependency-version: 1.18.7
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-04 06:47:21 +01:00
Lukasz Lenart 4d96950c51 docs: streamline CLAUDE.md based on benchmark findings (#1604)
Apply recommendations from CLAUDE.md benchmarking study (1,188 tests
across 3 models): remove redundant generic instructions, reframe
prohibitions as positive directives, trim inferable content, and
keep only project-specific knowledge that Claude cannot derive from
the codebase itself. Reduces file from 142 to 64 lines.

Key changes:
- Remove Common Pitfalls (negative framing, generic, duplicated)
- Remove Available Tools section (redundant with system prompt)
- Trim build commands to project-specific flags only
- Collapse Technology Stack into one-line overview
- Reframe security directives from "never do X" to "do Y instead"

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude <noreply@anthropic.com>
2026-03-02 11:31:16 +00:00
Lukasz Lenart 4d2eb93835 fix(core): correct isMethodSpecified() for wildcard-resolved methods (#1592)
DefaultActionProxy.resolveMethod() unconditionally set methodSpecified=false
when the method was not passed explicitly, including when it was resolved from
ActionConfig (e.g., wildcard substitution like method="{1}"). This caused
HttpMethodInterceptor to skip method-level annotation checks for wildcard
actions, falling back to class-level annotations instead.

Move methodSpecified=false inside the inner branch that defaults to "execute",
so config-resolved methods (including wildcard-substituted ones) correctly
report isMethodSpecified()=true. Update Javadoc to reflect the corrected
semantics.

Fixes [WW-5535](https://issues.apache.org/jira/browse/WW-5535)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude <noreply@anthropic.com>
2026-02-27 13:25:38 +01:00
dependabot[bot] 8ac1511290 build(deps): bump com.fasterxml.jackson:jackson-bom (#1597)
Bumps [com.fasterxml.jackson:jackson-bom](https://github.com/FasterXML/jackson-bom) from 2.21.0 to 2.21.1.
- [Commits](https://github.com/FasterXML/jackson-bom/compare/jackson-bom-2.21.0...jackson-bom-2.21.1)

---
updated-dependencies:
- dependency-name: com.fasterxml.jackson:jackson-bom
  dependency-version: 2.21.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-25 08:59:46 +01:00
dependabot[bot] ae94fd0d5d build(deps): bump maven-surefire-plugin.version from 3.5.4 to 3.5.5 (#1596)
Bumps `maven-surefire-plugin.version` from 3.5.4 to 3.5.5.

Updates `org.apache.maven.surefire:surefire-junit47` from 3.5.4 to 3.5.5

Updates `org.apache.maven.plugins:maven-surefire-plugin` from 3.5.4 to 3.5.5
- [Release notes](https://github.com/apache/maven-surefire/releases)
- [Commits](https://github.com/apache/maven-surefire/compare/surefire-3.5.4...surefire-3.5.5)

---
updated-dependencies:
- dependency-name: org.apache.maven.surefire:surefire-junit47
  dependency-version: 3.5.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: org.apache.maven.plugins:maven-surefire-plugin
  dependency-version: 3.5.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-25 08:59:35 +01:00
dependabot[bot] 3fc9efce5a build(deps-dev): bump org.apache.maven.plugins:maven-failsafe-plugin (#1595)
Bumps [org.apache.maven.plugins:maven-failsafe-plugin](https://github.com/apache/maven-surefire) from 3.5.4 to 3.5.5.
- [Release notes](https://github.com/apache/maven-surefire/releases)
- [Commits](https://github.com/apache/maven-surefire/compare/surefire-3.5.4...surefire-3.5.5)

---
updated-dependencies:
- dependency-name: org.apache.maven.plugins:maven-failsafe-plugin
  dependency-version: 3.5.5
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-25 08:59:24 +01:00
Lukasz Lenart ca740ed8fb WW-5514 Add StrutsProxyService for proxy detection and resolution (#1586)
* feat(proxy): WW-5514 add StrutsProxyService for proxy detection and resolution

Introduces a configurable ProxyService interface and StrutsProxyService
implementation for detecting and resolving Spring AOP/Hibernate proxies.

Key changes:
- Add ProxyService interface with isProxy, ultimateTargetClass, and
  resolveTargetMember methods
- Add StrutsProxyService implementation using configurable caches
- Add ProxyCacheFactory and StrutsProxyCacheFactory for cache management
- Integrate ProxyService into ChainingInterceptor, ParametersInterceptor,
  and SecurityMemberAccess
- Add integration test with Spring AOP proxied action chaining
- Add configuration constants for proxy cache type and size

The StrutsProxyService correctly handles:
- Spring CGLIB proxies (class-based)
- Spring JDK dynamic proxies (interface-based)
- Hibernate entity proxies
- Member resolution for allowlist checking

Fixes WW-5514

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* test(proxy): WW-5514 add ProxyService integration tests for Spring proxies

Add integration tests to SpringProxyUtilTest that verify the new
ProxyService works correctly with real Spring AOP proxies, alongside
the existing deprecated ProxyUtil tests.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(proxy): WW-5514 address PR review feedback for proxy caches

Remove targetClassCache from StrutsProxyService to avoid memory leak
(object-keyed cache reintroduced from PR #1578). Change default proxy
cache type to wtlfu to align with all other caches. Switch deprecated
ProxyUtil static caches to BASIC to remove hard Caffeine dependency.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-21 18:18:08 +01:00
Lukasz Lenart a9ce3e3c99 fix(convention): WW-4421 detect duplicate @Action names when execute() is annotated (#1579)
The duplicate @Action name detection in PackageBasedActionConfigBuilder
was embedded inside a conditional block that only ran when execute() was
NOT annotated with @Action. This meant two methods could map to the same
action name silently when execute() had an @Action annotation, with one
overwriting the other non-deterministically.

Extract the duplicate check to run unconditionally before the conditional
block, so it applies to all annotated methods regardless of whether
execute() is annotated.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude <noreply@anthropic.com>
2026-02-21 09:12:52 +00:00
dependabot[bot] fa34d2b02c build(deps): bump org.springframework:spring-framework-bom (#1588)
Bumps [org.springframework:spring-framework-bom](https://github.com/spring-projects/spring-framework) from 6.2.12 to 7.0.5.
- [Release notes](https://github.com/spring-projects/spring-framework/releases)
- [Commits](https://github.com/spring-projects/spring-framework/compare/v6.2.12...v7.0.5)

---
updated-dependencies:
- dependency-name: org.springframework:spring-framework-bom
  dependency-version: 7.0.5
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-21 09:12:08 +00:00
dependabot[bot] a21c763d8a build(deps): bump github/codeql-action from 4.32.3 to 4.32.4 (#1589)
Bumps [github/codeql-action](https://github.com/github/codeql-action) from 4.32.3 to 4.32.4.
- [Release notes](https://github.com/github/codeql-action/releases)
- [Commits](https://github.com/github/codeql-action/compare/v4.32.3...v4.32.4)

---
updated-dependencies:
- dependency-name: github/codeql-action
  dependency-version: 4.32.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-21 10:06:19 +01:00
Lukasz Lenart 2527ff15ff Merge pull request #1587 from apache/fix/dependabot-spring-exclusion
ci: use wildcard to exclude all Spring dependencies from 6.x updates
2026-02-21 09:53:08 +01:00
Lukasz Lenart 1eae40b817 ci: use wildcard to exclude all Spring dependencies from 6.x updates
🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2026-02-21 09:48:48 +01:00
dependabot[bot] f4d4ffe485 build(deps): bump org.springframework:spring-framework-bom (#1581)
Bumps [org.springframework:spring-framework-bom](https://github.com/spring-projects/spring-framework) from 6.2.12 to 7.0.4.
- [Release notes](https://github.com/spring-projects/spring-framework/releases)
- [Commits](https://github.com/spring-projects/spring-framework/compare/v6.2.12...v7.0.4)

---
updated-dependencies:
- dependency-name: org.springframework:spring-framework-bom
  dependency-version: 7.0.4
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-21 08:47:13 +00:00
Lukasz Lenart 182c00c083 conf(git): Ignores Cursor related paths (#1585) 2026-02-20 06:24:15 +00:00
dependabot[bot] d2810d42f0 build(deps): bump org.apache.commons:commons-fileupload2-jakarta-servlet6 (#1584)
Bumps org.apache.commons:commons-fileupload2-jakarta-servlet6 from 2.0.0-M4 to 2.0.0-M5.

---
updated-dependencies:
- dependency-name: org.apache.commons:commons-fileupload2-jakarta-servlet6
  dependency-version: 2.0.0-M5
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-18 17:17:39 +00:00
dependabot[bot] 71f25438e5 build(deps): bump github/codeql-action from 4.32.2 to 4.32.3 (#1580)
Bumps [github/codeql-action](https://github.com/github/codeql-action) from 4.32.2 to 4.32.3.
- [Release notes](https://github.com/github/codeql-action/releases)
- [Commits](https://github.com/github/codeql-action/compare/v4.32.2...v4.32.3)

---
updated-dependencies:
- dependency-name: github/codeql-action
  dependency-version: 4.32.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-18 08:41:39 +01:00
dependabot[bot] b704cb942e build(deps-dev): bump byte-buddy.version from 1.18.4 to 1.18.5 (#1583)
Bumps `byte-buddy.version` from 1.18.4 to 1.18.5.

Updates `net.bytebuddy:byte-buddy` from 1.18.4 to 1.18.5
- [Release notes](https://github.com/raphw/byte-buddy/releases)
- [Changelog](https://github.com/raphw/byte-buddy/blob/master/release-notes.md)
- [Commits](https://github.com/raphw/byte-buddy/compare/byte-buddy-1.18.4...byte-buddy-1.18.5)

Updates `net.bytebuddy:byte-buddy-agent` from 1.18.4 to 1.18.5
- [Release notes](https://github.com/raphw/byte-buddy/releases)
- [Changelog](https://github.com/raphw/byte-buddy/blob/master/release-notes.md)
- [Commits](https://github.com/raphw/byte-buddy/compare/byte-buddy-1.18.4...byte-buddy-1.18.5)

---
updated-dependencies:
- dependency-name: net.bytebuddy:byte-buddy
  dependency-version: 1.18.5
  dependency-type: direct:development
  update-type: version-update:semver-patch
- dependency-name: net.bytebuddy:byte-buddy-agent
  dependency-version: 1.18.5
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-18 08:41:25 +01:00
Lukasz Lenart c90bb70c25 feat(ui): WW-3429 add configurable checkbox hidden field prefix (#1570)
Add struts.ui.checkbox.hiddenPrefix constant to allow configuring
the checkbox hidden field prefix, addressing HTML validation warnings
about double underscores while maintaining backward compatibility.

Changes:
- Add STRUTS_UI_CHECKBOX_HIDDEN_PREFIX constant to StrutsConstants
- Add default value __checkbox_ to default.properties
- Update Checkbox component to inject and pass prefix to templates
- Update CheckboxInterceptor to use configurable prefix
- Update simple/checkbox.ftl and html5/checkbox.ftl templates
- Update CheckboxHandler in javatemplates plugin
- Add tests for configurable prefix functionality
- Fix bug in CheckboxHandler where value was incorrectly prefixed

Configuration example:
  struts.ui.checkbox.hiddenPrefix=struts_checkbox_

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude <noreply@anthropic.com>
2026-02-17 07:14:03 +01:00
Lukasz Lenart a0a213f7c5 feat(security): WW-5294 add warning when JSP tags accessed directly (#1569)
Add security warning to TagUtils.getStack() that logs when JSP tags
are rendered outside of action scope (direct JSP access). This helps
developers identify potential security issues where JSPs are accessed
directly without going through the Struts action flow.

The warning message includes a link to the security documentation at
https://struts.apache.org/security/#never-expose-jsp-files-directly

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude <noreply@anthropic.com>
2026-02-17 07:13:33 +01:00
dependabot[bot] e3d09dfc47 WW-5613 build(deps): bump ognl:ognl from 3.4.8 to 3.4.10 (#1567)
OgntUtil has been extended to properly pass root object if needed

Bumps [ognl:ognl](https://github.com/orphan-oss/ognl) from 3.4.8 to 3.4.10.
- [Release notes](https://github.com/orphan-oss/ognl/releases)
- [Commits](https://github.com/orphan-oss/ognl/commits)

---
updated-dependencies:
- dependency-name: ognl:ognl
  dependency-version: 3.4.10
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-17 07:13:05 +01:00
dependabot[bot] accd4c7e5a build(deps): bump org.apache.maven.plugins:maven-dependency-plugin (#1575)
Bumps [org.apache.maven.plugins:maven-dependency-plugin](https://github.com/apache/maven-dependency-plugin) from 3.9.0 to 3.10.0.
- [Release notes](https://github.com/apache/maven-dependency-plugin/releases)
- [Commits](https://github.com/apache/maven-dependency-plugin/compare/maven-dependency-plugin-3.9.0...maven-dependency-plugin-3.10.0)

---
updated-dependencies:
- dependency-name: org.apache.maven.plugins:maven-dependency-plugin
  dependency-version: 3.10.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-12 18:52:43 +02:00
dependabot[bot] ea906a22e4 build(deps): bump github/codeql-action from 4.32.1 to 4.32.2 (#1574)
Bumps [github/codeql-action](https://github.com/github/codeql-action) from 4.32.1 to 4.32.2.
- [Release notes](https://github.com/github/codeql-action/releases)
- [Commits](https://github.com/github/codeql-action/compare/v4.32.1...v4.32.2)

---
updated-dependencies:
- dependency-name: github/codeql-action
  dependency-version: 4.32.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-12 18:52:21 +02:00
Kusal Kithul-Godage fe69771562 Merge pull request #1578 from apache/WW-5614-proxy-memory
WW-5614 Remove cache for ProxyUtil#ultimateTargetClass
2026-02-12 21:58:11 +11:00
Kusal Kithul-Godage 11bc215c94 WW-5614 Remove cache for ProxyUtil#ultimateTargetClass 2026-02-12 12:46:17 +11:00
171 changed files with 10314 additions and 1925 deletions
-330
View File
@@ -1,330 +0,0 @@
---
name: test-runner
description: Execute and analyze Maven tests for Apache Struts. Use PROACTIVELY when code changes, user mentions testing, asks to run tests, check test coverage, or validate changes. MUST BE USED for all Maven test execution (mvn test), test result analysis, and coverage reports.
tools: bash_tool, view, str_replace
model: sonnet
color: green
---
# Apache Struts Test Runner Agent
## Identity
You are a specialized test execution and analysis expert for Apache Struts projects. You understand the framework's testing patterns, Maven module structure, and can intelligently execute tests, analyze results, and provide actionable feedback on test coverage and quality.
## Core Capabilities
### 1. Intelligent Test Execution
- **Module-aware testing**: Execute tests in specific Maven modules (`core/`, `plugins/`, `apps/`, `jakarta/`)
- **Pattern-based test selection**: Run tests matching specific patterns or components
- **Incremental testing**: Execute only tests affected by recent changes
- **Performance testing**: Measure test execution times and identify slow tests
- **Parallel execution**: Optimize test runs using Maven parallel execution
### 2. Test Analysis and Reporting
- **Coverage analysis**: Analyze test coverage across different components
- **Failure analysis**: Categorize test failures and provide remediation guidance
- **Security test validation**: Ensure security-related tests are comprehensive
- **Integration test coordination**: Manage complex integration test scenarios
- **Regression detection**: Identify potential regressions in test results
### 3. Struts-Specific Testing Expertise
- **Action testing patterns**: Validate ActionSupport and POJO action tests
- **Interceptor testing**: Comprehensive interceptor chain testing
- **Result type testing**: Validate result implementations and configurations
- **Plugin testing**: Coordinate plugin-specific test execution
- **Configuration testing**: Validate struts.xml and plugin configurations
## Maven Test Execution Strategies
### 1. Basic Test Commands
```bash
# Run all tests efficiently (skip assembly to avoid docs/examples ZIP creation)
mvn test -DskipAssembly
# Run tests for specific module
mvn test -pl core -DskipAssembly
mvn test -pl plugins/json -DskipAssembly
mvn test -pl apps/showcase -DskipAssembly
# Build without running tests (for dependency verification)
mvn clean install -DskipTests
```
### 2. Pattern-Based Test Selection
```bash
# Run specific test class
mvn test -Dtest=JakartaMultiPartRequestTest -DskipAssembly
# Run tests matching pattern
mvn test -Dtest=*MultiPartRequestTest -DskipAssembly
mvn test -Dtest=*Security*Test -DskipAssembly
mvn test -Dtest=*Interceptor*Test -DskipAssembly
# Run tests with specific method pattern
mvn test -Dtest=*MultiPartRequestTest#temporal* -DskipAssembly
mvn test -Dtest=ActionSupportTest#testValidation* -DskipAssembly
```
### 3. Advanced Test Execution
```bash
# Run tests with coverage analysis
mvn clean test jacoco:report -DskipAssembly
# Run tests in specific profile
mvn test -P integration-tests -DskipAssembly
# Run tests with specific Maven properties
mvn test -Dmaven.surefire.debug -DskipAssembly
# Parallel test execution
mvn test -T 1C -DskipAssembly
```
## Test Analysis Framework
### 1. Test Result Categorization
**Pass Categories:**
- ✅ Unit tests (individual component testing)
- ✅ Integration tests (multi-component interaction)
- ✅ Security tests (vulnerability and attack prevention)
- ✅ Performance tests (response time and throughput)
- ✅ Configuration tests (XML and annotation validation)
**Failure Categories:**
- 🔴 Critical failures (security or core functionality)
- 🟠 High-priority failures (major feature breakdown)
- 🟡 Medium-priority failures (minor feature issues)
- 🔵 Low-priority failures (documentation or cosmetic)
### 2. Coverage Analysis Approach
```bash
# Generate coverage reports
mvn clean test jacoco:report -DskipAssembly
# Analyze coverage by component type
find target/site/jacoco -name "*.html" | grep -E "(action|interceptor|result)"
# Check critical security component coverage
grep -r "org.apache.struts2.interceptor.parameter" target/site/jacoco/
grep -r "org.apache.struts2.ognl" target/site/jacoco/
```
### 3. Performance Test Analysis
- Monitor test execution times across modules
- Identify performance regressions in test suite
- Analyze memory usage during test execution
- Validate performance requirements for specific components
## Struts Testing Patterns
### 1. Action Testing Patterns
```java
// StrutsTestCase pattern for action testing
public class MyActionTest extends StrutsTestCase {
public void testActionExecution() throws Exception {
MyAction action = new MyAction();
String result = action.execute();
assertEquals("success", result);
}
}
// Mock-based testing pattern
@Test
public void testWithMocks() {
ActionContext context = mock(ActionContext.class);
// Test implementation
}
```
### 2. Interceptor Testing Patterns
```java
// Interceptor testing with MockActionInvocation
@Test
public void testInterceptor() throws Exception {
MockActionInvocation mai = new MockActionInvocation();
String result = interceptor.intercept(mai);
assertEquals("success", result);
}
```
### 3. Security Testing Patterns
```java
// OGNL injection prevention tests
@Test
public void testOgnlInjectionPrevention() {
String maliciousInput = "%{#context['xwork.MethodAccessor.denyMethodExecution']=false}";
// Verify input is properly filtered
}
// Parameter filtering tests
@Test
public void testParameterFiltering() {
Map<String, Object> params = new HashMap<>();
params.put("class.classLoader.resources", "malicious");
// Verify parameter is excluded
}
```
## Test Execution Workflows
### 1. Pre-commit Testing
```bash
# Quick smoke tests before commit
mvn test -Dtest=*Smoke*Test -DskipAssembly
# Security-focused tests
mvn test -Dtest=*Security*Test,*Ognl*Test,*Parameter*Test -DskipAssembly
# Core functionality tests
mvn test -pl core -Dtest=*Action*Test,*Interceptor*Test -DskipAssembly
```
### 2. Feature-Specific Testing
```bash
# File upload feature tests
mvn test -Dtest=*FileUpload*Test,*MultiPart*Test -DskipAssembly
# Validation framework tests
mvn test -Dtest=*Validation*Test,*Validator*Test -DskipAssembly
# Plugin integration tests
mvn test -pl plugins/json -DskipAssembly
mvn test -pl plugins/rest -DskipAssembly
```
### 3. Comprehensive Release Testing
```bash
# Full test suite execution
mvn clean install -DskipAssembly
# Integration tests across all modules
mvn test -P integration-tests -DskipAssembly
# Performance regression testing
mvn test -P performance-tests -DskipAssembly
```
## Test Report Generation
### 1. Standard Test Reports
```bash
# Generate Surefire reports
mvn surefire-report:report -DskipAssembly
# Generate Failsafe reports (integration tests)
mvn failsafe:report -DskipAssembly
# Generate combined test report
mvn surefire-report:report-only failsafe:report-only -DskipAssembly
```
### 2. Coverage Reports
```bash
# JaCoCo coverage report
mvn jacoco:report
# Coverage by module
mvn jacoco:report -pl core
mvn jacoco:report -pl plugins/json
```
### 3. Custom Test Analysis
- Parse test output for specific patterns
- Generate security test compliance reports
- Analyze test execution trends over time
- Identify flaky or unstable tests
## Output Format
Structure test results as:
```
## Test Execution Report
### Summary
- **Total Tests**: [number]
- **Passed**: [number] ✅
- **Failed**: [number] ❌
- **Skipped**: [number] ⏭️
- **Execution Time**: [duration]
### Module Results
#### Core Module
- Tests: [passed/total]
- Key Failures: [list critical failures]
- Coverage: [percentage]
#### Plugins
- [Plugin]: [passed/total]
- Notable Issues: [any plugin-specific issues]
### Failure Analysis
#### Critical Failures (🔴)
1. **[TestClass.testMethod]**
- **Error**: [failure message]
- **Impact**: [functional impact]
- **Remediation**: [fix suggestions]
### Security Test Status
- OGNL injection tests: [status]
- Parameter filtering tests: [status]
- File upload security tests: [status]
### Coverage Analysis
- **Overall Coverage**: [percentage]
- **Security Components**: [percentage]
- **Critical Paths**: [percentage]
### Performance Analysis
- **Slowest Tests**: [list with times]
- **Module Performance**: [execution time by module]
- **Regression Indicators**: [any performance issues]
### Recommendations
- [Specific actions to address failures]
- [Coverage improvement suggestions]
- [Performance optimization recommendations]
```
## Integration Points
### 1. Maven Module Structure
- **Core module** (`/core/`): Framework core tests
- **Plugin modules** (`/plugins/*/`): Plugin-specific tests
- **Application modules** (`/apps/*/`): Integration and example tests
- **Jakarta module** (`/jakarta/`): Jakarta EE compatibility tests
### 2. Test Categories
- **Unit tests**: Fast, isolated component tests
- **Integration tests**: Multi-component interaction tests
- **Security tests**: Vulnerability and attack prevention tests
- **Performance tests**: Load and stress testing
- **Configuration tests**: XML and annotation validation
### 3. CI/CD Integration
- Pre-commit hook validation
- Pull request test automation
- Release candidate testing
- Performance regression detection
## Best Practices
### 1. Test Execution Efficiency
- Always use `-DskipAssembly` to avoid building documentation/examples
- Use pattern matching to run relevant tests only
- Leverage parallel execution for large test suites
- Cache dependencies to reduce setup time
### 2. Test Quality Assurance
- Ensure security tests cover all attack vectors
- Validate test coverage meets minimum thresholds
- Monitor test execution trends for performance regressions
- Maintain test isolation and repeatability
### 3. Failure Handling
- Categorize failures by severity and impact
- Provide clear remediation guidance
- Track failure patterns across builds
- Implement automatic retry for flaky tests
Remember: Testing is crucial for Struts applications due to the framework's security sensitivity. Always prioritize security tests and ensure comprehensive coverage of OGNL evaluation paths and parameter handling logic.
+2 -1
View File
@@ -29,9 +29,10 @@ updates:
- dependency-name: "opensymphony:sitemesh"
- dependency-name: "net.sf.jasperreports:jasperreports"
- dependency-name: "javax.enterprise:cdi-api"
- dependency-name: "org.springframework:spring-core"
- dependency-name: "org.springframework:*"
- dependency-name: "org.apache.struts:struts-annotations"
- dependency-name: "org.apache.juneau:juneau-marshall"
- dependency-name: "org.apache.tomcat:tomcat-api"
- dependency-name: "org.apache.tomcat:tomcat-juli"
- dependency-name: "org.apache.tomcat:tomcat-jasper"
- dependency-name: "org.apache.rat:apache-rat-plugin"
+3 -3
View File
@@ -52,12 +52,12 @@ jobs:
java-version: 17
cache: 'maven'
- name: Initialize CodeQL
uses: github/codeql-action/init@v4.32.1
uses: github/codeql-action/init@v4.35.1
with:
languages: ${{ matrix.language }}
- name: Autobuild
uses: github/codeql-action/autobuild@v4.32.1
uses: github/codeql-action/autobuild@v4.35.1
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v4.32.1
uses: github/codeql-action/analyze@v4.35.1
with:
category: "/language:${{matrix.language}}"
+2 -2
View File
@@ -58,13 +58,13 @@ jobs:
publish_results: true
- name: "Upload artifact"
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # 6.0.0
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # 7.0.0
with:
name: SARIF file
path: results.sarif
retention-days: 5
- name: "Upload to code-scanning"
uses: github/codeql-action/upload-sarif@ab5b0e3aabf4de044f07a63754c2110d3ef2df38 # 2.22.11
uses: github/codeql-action/upload-sarif@c618c9bddbf8ce520050acf14e9bb6c220e22931 # 2.22.11
with:
sarif_file: results.sarif
+5
View File
@@ -49,3 +49,8 @@ test-output
# Claude Code local settings
.claude/settings.local.json
# Cursor + Metals
.cursor/
.bloop/
.metals/
+11 -79
View File
@@ -6,38 +6,19 @@ For detailed procedures, use the specialized agents and commands in `.claude/age
## Project Overview
Apache Struts is a mature MVC web application framework for Java (originally WebWork 2). Current version: *
*7.2.0-SNAPSHOT**.
Apache Struts is a mature MVC web application framework for Java (originally WebWork 2). Current version: **7.2.0-SNAPSHOT**. Uses OGNL for value stack expressions and FreeMarker for UI tag templates.
### Build Commands
```bash
# Full build with tests
mvn clean install
# Run all tests (faster, skips assembly)
# Run tests (skip assembly for speed)
mvn test -DskipAssembly
# Run single test class
mvn test -DskipAssembly -Dtest=MyClassTest
# Single test in specific module
mvn test -DskipAssembly -pl core -Dtest=MyClassTest#testMethodName
# Run single test method
mvn test -DskipAssembly -Dtest=MyClassTest#testMethodName
# Run tests in a specific module
mvn test -DskipAssembly -pl core
# Build without tests
mvn clean install -DskipTests
# Build with code coverage (JaCoCo)
mvn clean install -Pcoverage
# Build with Jakarta EE 11 (Spring 7)
# Jakarta EE 11 / Spring 7 profile
mvn clean install -Pjakartaee11
# Run OWASP dependency vulnerability check
mvn verify -Pdependency-check
```
### Project Structure
@@ -57,13 +38,6 @@ struts/
**Request Lifecycle**: `Dispatcher``ActionProxy``ActionInvocation` → Interceptor stack → `Action` → Result
Key components:
- **ActionSupport**: Base class for actions (validation, i18n, messages)
- **ActionContext**: Thread-local context with request/response/session data
- **Interceptors**: Cross-cutting concerns (validation, file upload, security, params)
- **Results**: Response handlers (dispatcher, redirect, json, stream)
Key packages in `org.apache.struts2`:
- `dispatcher` - Request handling, `Dispatcher`, servlet integration
@@ -72,22 +46,14 @@ Key packages in `org.apache.struts2`:
- `action` - Action interfaces (`UploadedFilesAware`, `SessionAware`, etc.)
- `security` - Security utilities and OGNL member access policies
### Technology Stack
- **Java 17+** with Jakarta EE 10 (Servlet 6.0, JSP 3.1)
- **OGNL** - Expression language for value stack access
- **FreeMarker** - Default template engine for UI tags
- **Commons FileUpload2** - File upload handling
- **Log4j2/SLF4J** - Logging
## Security-Critical Patterns
Apache Struts has a history of security vulnerabilities. Follow these strictly:
Apache Struts has a history of security vulnerabilities (OGNL injection, temp file exploits). Apply these Struts-specific patterns:
1. **Temporary files**: Never use system temp directory; use UUID-based names in controlled locations
2. **OGNL expressions**: Never evaluate user-controlled OGNL; use allowlist member access
1. **Temporary files**: Use UUID-based names in controlled locations (see example below)
2. **OGNL expressions**: Evaluate only framework-generated OGNL; use allowlist member access
3. **File uploads**: Validate content types, sanitize filenames, enforce size limits
4. **Parameter injection**: Use `ParameterNameAware` to filter dangerous parameter names
4. **Parameter filtering**: Use `ParameterNameAware` to restrict accepted parameter names
```java
// Secure temporary file pattern
@@ -97,46 +63,12 @@ protected File createTemporaryFile(String fileName, Path location) {
}
```
Run `/security_scan` for comprehensive security analysis.
## Testing
**Priority order for running tests:**
1. **JetBrains MCP** (in IntelliJ): `mcp__jetbrains__execute_run_configuration`
2. **test-runner agent**: `Task` tool with `subagent_type="test-runner"`
3. **Direct Maven**: `mvn test -DskipAssembly -Dtest=TestClassName`
Tests use JUnit 5 with AssertJ assertions and Mockito for mocking.
## Available Tools
### Commands
- `/security_scan` - OGNL injection, CVE detection, security analysis
- `/quality_check` - JavaDoc compliance, coding standards
- `/config_analyze` - struts.xml validation, interceptor analysis
- `/create_plan` / `/validate_plan` - Implementation planning
- `/research_codebase` - Codebase exploration
### Specialized Agents
- `test-runner` - Maven test execution (use this to RUN tests)
- `security-analyzer` - Security vulnerability scanning
- `codebase-locator` - Find files, classes, implementations
- `codebase-pattern-finder` - Find similar code patterns
- `config-validator` - Validate Struts configuration files
Tests use JUnit 5 with AssertJ assertions and Mockito for mocking. Run with `mvn test -DskipAssembly`.
## Pull Requests
- **Title format**: `WW-XXXX Description` (Jira ticket ID required)
- **Link ticket in description**: `Fixes [WW-XXXX](https://issues.apache.org/jira/browse/WW-XXXX)`
- **Issue tracker**: https://issues.apache.org/jira/projects/WW
## Common Pitfalls
1. Never use `File.createTempFile()` without controlling the directory
2. Always clean up temporary files (track and delete in finally blocks)
3. Test error paths and cleanup behavior, not just happy paths
4. Don't catch generic `Exception` - catch specific types
5. Use `protected` visibility for methods subclasses may override
- **Issue tracker**: https://issues.apache.org/jira/projects/WW
+18
View File
@@ -0,0 +1,18 @@
# Rest Showcase
> **WARNING:** This application is a demonstration/development tool only. It is **NOT** intended for production
> deployment. Deploying this application on a publicly accessible server may pose security risks.
Rest Showcase is a simple example of a REST app built with the REST plugin.
For more on getting started with Struts, see:
- https://struts.apache.org/getting-started/
## I18N
Please note that this project was created with the assumption that it will be run in an environment where the default
locale is set to English. This means that the default messages defined in `package.properties` are in English.
If the default locale for your server is different, then rename `package.properties` to `package_en.properties` and
create a new `package.properties` with proper values for your default locale.
-15
View File
@@ -1,15 +0,0 @@
README.txt - Rest Showcase Webapp
Rest Showcase is a simple example of REST app build with the REST plugin.
For more on getting started with Struts, see
* http://cwiki.apache.org/WW/home.html
I18N:
=====
Please note that this project was created with the assumption that it will be run
in an environment where the default locale is set to English. This means that
the default messages defined in package.properties are in English. If the default
locale for your server is different, then rename package.properties to package_en.properties
and create a new package.properties with proper values for your default locale.
+19
View File
@@ -0,0 +1,19 @@
# Showcase
> **WARNING:** This application is a demonstration/development tool only. It is **NOT** intended for production
> deployment. It contains features such as source code viewing that intentionally expose internal application details.
> Deploying this application on a publicly accessible server may pose security risks.
Showcase is a collection of examples with code that you might adopt and adapt in your own applications.
For more on getting started with Struts, see:
- https://struts.apache.org/getting-started/
## I18N
Please note that this project was created with the assumption that it will be run in an environment where the default
locale is set to English. This means that the default messages defined in `package.properties` are in English.
If the default locale for your server is different, then rename `package.properties` to `package_en.properties` and
create a new `package.properties` with proper values for your default locale.
-16
View File
@@ -1,16 +0,0 @@
README.txt - showcase
Showcase is a collection of examples with code that you might be adopt and
adapt in your own applications.
For more on getting started with Struts, see
* http://cwiki.apache.org/WW/home.html
I18N:
=====
Please note that this project was created with the assumption that it will be run
in an environment where the default locale is set to English. This means that
the default messages defined in package.properties are in English. If the default
locale for your server is different, then rename package.properties to package_en.properties
and create a new package.properties with proper values for your default locale.
+5 -1
View File
@@ -119,6 +119,10 @@
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-slf4j-impl</artifactId>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-web</artifactId>
</dependency>
<dependency>
<groupId>org.sitemesh</groupId>
@@ -207,7 +211,7 @@
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-failsafe-plugin</artifactId>
<version>3.5.4</version>
<version>3.5.5</version>
<configuration>
<includes>
<include>it.org.apache.struts2.showcase.*Test</include>
@@ -0,0 +1,42 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.struts2.showcase.proxy;
import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
/**
* Simple AOP interceptor that wraps actions in a Spring proxy.
* Used to test that Struts correctly handles Spring AOP proxied actions
* in action chaining scenarios (WW-5514).
*/
public class LoggingInterceptor implements MethodInterceptor {
private static final Logger LOG = LogManager.getLogger(LoggingInterceptor.class);
@Override
public Object invoke(MethodInvocation invocation) throws Throwable {
LOG.debug("Invoking method: {} on target: {}",
invocation.getMethod().getName(),
invocation.getThis().getClass().getName());
return invocation.proceed();
}
}
@@ -30,7 +30,9 @@ import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.URL;
import java.net.URI;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
@@ -90,7 +92,11 @@ public class ViewSourceAction extends ActionSupport implements ServletContextAwa
if (config != null && config.startsWith("file:/")) {
int pos = config.lastIndexOf(':');
configLine = Integer.parseInt(config.substring(pos + 1));
configLines = read(new URL(config.substring(0, pos)).openStream(), configLine);
String fileUrl = config.substring(0, pos);
Path configPath = resolveAllowedConfigPath(fileUrl);
if (configPath != null) {
configLines = read(Files.newInputStream(configPath), configLine);
}
}
return SUCCESS;
}
@@ -227,6 +233,29 @@ public class ViewSourceAction extends ActionSupport implements ServletContextAwa
return snippet;
}
/**
* Resolves the given file URL to a real path if it points to an XML file within the webapp's
* deployment directory, preventing arbitrary file reads via crafted config parameters.
*
* @return the resolved path if allowed, or null if the path is outside the webapp or not an XML file
*/
private Path resolveAllowedConfigPath(String fileUrl) {
try {
Path filePath = Path.of(new URI(fileUrl)).toRealPath();
String realBasePath = servletContext.getRealPath("/");
if (realBasePath == null) {
return null;
}
Path basePath = Path.of(realBasePath).toRealPath();
if (filePath.startsWith(basePath) && filePath.toString().endsWith(".xml")) {
return filePath;
}
return null;
} catch (Exception e) {
return null;
}
}
@Override
public void withServletContext(ServletContext arg0) {
this.servletContext = arg0;
@@ -20,21 +20,26 @@
*/
-->
<!DOCTYPE struts PUBLIC
"-//Apache Software Foundation//DTD Struts Configuration 6.0//EN"
"https://struts.apache.org/dtds/struts-6.0.dtd">
"-//Apache Software Foundation//DTD Struts Configuration 6.0//EN"
"https://struts.apache.org/dtds/struts-6.0.dtd">
<struts>
<package name="actionchaining" extends="struts-default" namespace="/actionchaining">
<action name="actionChain1" class="org.apache.struts2.showcase.actionchaining.ActionChain1">
<result type="chain">actionChain2</result>
</action>
<action name="actionChain2" class="org.apache.struts2.showcase.actionchaining.ActionChain2">
<result type="chain">actionChain3</result>
</action>
<action name="actionChain3" class="org.apache.struts2.showcase.actionchaining.ActionChain3">
<result>/WEB-INF/actionchaining/actionChainingResult.jsp</result>
</action>
</package>
<package name="actionchaining" extends="struts-default" namespace="/actionchaining">
<action name="actionChain1" class="org.apache.struts2.showcase.actionchaining.ActionChain1">
<result type="chain">actionChain2</result>
</action>
<action name="actionChain2" class="org.apache.struts2.showcase.actionchaining.ActionChain2">
<result type="chain">actionChain3</result>
</action>
<action name="actionChain3" class="org.apache.struts2.showcase.actionchaining.ActionChain3">
<result>/WEB-INF/actionchaining/actionChainingResult.jsp</result>
</action>
<!-- Spring AOP Proxied Action Chain Test (WW-5514) -->
<action name="proxiedActionChain1" class="proxiedActionChain1">
<result type="chain">actionChain2</result>
</action>
</package>
</struts>
+50 -43
View File
@@ -20,83 +20,88 @@
*/
-->
<!DOCTYPE struts PUBLIC
"-//Apache Software Foundation//DTD Struts Configuration 6.0//EN"
"https://struts.apache.org/dtds/struts-6.0.dtd">
"-//Apache Software Foundation//DTD Struts Configuration 6.0//EN"
"https://struts.apache.org/dtds/struts-6.0.dtd">
<!-- START SNIPPET: xworkSample -->
<struts>
<!-- Some or all of these can be flipped to true for debugging -->
<constant name="struts.i18n.reload" value="false" />
<constant name="struts.enable.DynamicMethodInvocation" value="true" />
<constant name="struts.devMode" value="false" />
<constant name="struts.configuration.xml.reload" value="false" />
<constant name="struts.custom.i18n.resources" value="globalMessages" />
<constant name="struts.action.extension" value="action,," />
<constant name="struts.i18n.reload" value="false"/>
<constant name="struts.enable.DynamicMethodInvocation" value="true"/>
<constant name="struts.devMode" value="false"/>
<constant name="struts.configuration.xml.reload" value="false"/>
<constant name="struts.custom.i18n.resources" value="globalMessages"/>
<constant name="struts.action.extension" value="action,,"/>
<constant name="struts.allowlist.enable" value="true" />
<constant name="struts.parameters.requireAnnotations" value="true" />
<constant name="struts.allowlist.enable" value="true"/>
<constant name="struts.parameters.requireAnnotations" value="true"/>
<constant name="struts.allowlist.packageNames" value="org.apache.struts2.showcase"/>
<constant name="struts.convention.package.locators.basePackage" value="org.apache.struts2.showcase" />
<constant name="struts.convention.result.path" value="/WEB-INF" />
<!-- Enable Spring AOP proxy support for action chaining test (WW-5514) -->
<constant name="struts.disallowProxyObjectAccess" value="false"/>
<constant name="struts.convention.package.locators.basePackage" value="org.apache.struts2.showcase"/>
<constant name="struts.convention.result.path" value="/WEB-INF"/>
<!-- Necessary for Showcase because default includes org.apache.struts2.* -->
<constant name="struts.convention.exclude.packages" value="org.apache.struts.*,org.springframework.web.struts.*,org.springframework.web.struts2.*,org.hibernate.*"/>
<constant name="struts.convention.exclude.packages"
value="org.apache.struts.*,org.springframework.web.struts.*,org.springframework.web.struts2.*,org.hibernate.*"/>
<constant name="struts.freemarker.manager.classname" value="customFreemarkerManager" />
<constant name="struts.serve.static" value="true" />
<constant name="struts.serve.static.browserCache" value="false" />
<constant name="struts.freemarker.manager.classname" value="customFreemarkerManager"/>
<constant name="struts.serve.static" value="true"/>
<constant name="struts.serve.static.browserCache" value="false"/>
<constant name="struts.action.excludePattern" value=".*/images/.*\.gif,.*/img/.*\.gif,.*/styles/.*\.css,.*/js/.*\.js,/testServlet/.*"/>
<constant name="struts.action.excludePattern"
value=".*/images/.*\.gif,.*/img/.*\.gif,.*/styles/.*\.css,.*/js/.*\.js,/testServlet/.*"/>
<include file="struts-interactive.xml" />
<include file="struts-interactive.xml"/>
<include file="struts-hangman.xml" />
<include file="struts-hangman.xml"/>
<include file="struts-tags.xml"/>
<include file="struts-validation.xml" />
<include file="struts-validation.xml"/>
<include file="struts-actionchaining.xml" />
<include file="struts-actionchaining.xml"/>
<include file="struts-fileupload.xml" />
<include file="struts-fileupload.xml"/>
<include file="struts-person.xml" />
<include file="struts-person.xml"/>
<include file="struts-wait.xml" />
<include file="struts-wait.xml"/>
<include file="struts-token.xml" />
<include file="struts-token.xml"/>
<include file="struts-model-driven.xml" />
<include file="struts-model-driven.xml"/>
<include file="struts-filedownload.xml" />
<include file="struts-filedownload.xml"/>
<include file="struts-conversion.xml" />
<include file="struts-conversion.xml"/>
<include file="struts-freemarker.xml" />
<include file="struts-freemarker.xml"/>
<include file="struts-tiles.xml" />
<include file="struts-tiles.xml"/>
<include file="struts-xslt.xml" />
<include file="struts-xslt.xml"/>
<include file="struts-async.xml" />
<include file="struts-async.xml"/>
<include file="struts-dispatcher.xml" />
<include file="struts-dispatcher.xml"/>
<include file="struts-params-annotation.xml" />
<include file="struts-params-annotation.xml"/>
<package name="default" extends="struts-default">
<interceptors>
<interceptor-stack name="crudStack">
<interceptor-ref name="checkbox" />
<interceptor-ref name="params" />
<interceptor-ref name="staticParams" />
<interceptor-ref name="defaultStack" />
<interceptor-ref name="checkbox"/>
<interceptor-ref name="params"/>
<interceptor-ref name="staticParams"/>
<interceptor-ref name="defaultStack"/>
</interceptor-stack>
</interceptors>
<default-action-ref name="showcase" />
<default-action-ref name="showcase"/>
<action name="showcase">
<result>/WEB-INF/showcase.jsp</result>
@@ -125,7 +130,7 @@
</action>
<action name="edit" class="org.apache.struts2.showcase.action.SkillAction">
<result>/WEB-INF/empmanager/editSkill.jsp</result>
<interceptor-ref name="params" />
<interceptor-ref name="params"/>
<interceptor-ref name="basicStack"/>
</action>
<action name="save" class="org.apache.struts2.showcase.action.SkillAction" method="save">
@@ -146,9 +151,11 @@
<interceptor-ref name="basicStack"/>
</action>
<action name="edit-*" class="org.apache.struts2.showcase.action.EmployeeAction">
<param name="empId">{1}</param>
<param name="empId">{1}</param>
<result>/WEB-INF/empmanager/editEmployee.jsp</result>
<interceptor-ref name="crudStack"><param name="validation.excludeMethods">execute</param></interceptor-ref>
<interceptor-ref name="crudStack">
<param name="validation.excludeMethods">execute</param>
</interceptor-ref>
</action>
<action name="save" class="org.apache.struts2.showcase.action.EmployeeAction" method="save">
<result name="input">/WEB-INF/empmanager/editEmployee.jsp</result>
@@ -168,5 +175,5 @@
</struts>
<!-- END SNIPPET: xworkSample -->
<!-- END SNIPPET: xworkSample -->
@@ -115,5 +115,25 @@
<bean id="guessCharacterAction" class="org.apache.struts2.showcase.hangman.GuessCharacterAction" scope="prototype"/>
<bean id="getUpdatedHangmanAction" class="org.apache.struts2.showcase.hangman.GetUpdatedHangmanAction"
scope="prototype"/>
<!-- Spring AOP Proxy Configuration for Action Chaining Test (WW-5514) -->
<bean id="loggingInterceptor" class="org.apache.struts2.showcase.proxy.LoggingInterceptor"/>
<bean class="org.springframework.aop.framework.autoproxy.BeanNameAutoProxyCreator">
<property name="proxyTargetClass" value="true"/>
<property name="beanNames">
<list>
<value>proxiedActionChain1</value>
</list>
</property>
<property name="interceptorNames">
<list>
<value>loggingInterceptor</value>
</list>
</property>
</bean>
<bean id="proxiedActionChain1" class="org.apache.struts2.showcase.actionchaining.ActionChain1" scope="prototype"/>
</beans>
@@ -0,0 +1,67 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package it.org.apache.struts2.showcase;
import org.htmlunit.WebClient;
import org.htmlunit.html.HtmlPage;
import org.junit.Test;
import static org.junit.Assert.assertTrue;
/**
* Integration test verifying that Spring AOP proxied actions work correctly
* with action chaining. This tests the WW-5514 StrutsProxyService integration.
*
* <p>The test uses a Spring AOP proxied version of ActionChain1 (proxiedActionChain1)
* which is wrapped by {@link org.apache.struts2.showcase.proxy.LoggingInterceptor}.
* The ChainingInterceptor must correctly resolve the target class through
* StrutsProxyService to copy properties to the next action in the chain.</p>
*/
public class SpringProxyActionChainingTest {
/**
* Tests that action chaining works correctly when the first action is a Spring AOP proxy.
*
* <p>This verifies that:
* <ul>
* <li>StrutsProxyService correctly identifies the Spring CGLIB proxy</li>
* <li>ChainingInterceptor resolves the target class for property copying</li>
* <li>Properties from the proxied ActionChain1 are correctly copied to ActionChain2</li>
* </ul>
* </p>
*/
@Test
public void testProxiedActionChaining() throws Exception {
try (final WebClient webClient = new WebClient()) {
final HtmlPage page = webClient.getPage(
ParameterUtils.getBaseUrl() + "/actionchaining/proxiedActionChain1!input"
);
final String pageAsText = page.asNormalizedText();
// Verify properties were chained correctly despite proxy
assertTrue("ActionChain1 property should be present",
pageAsText.contains("Action Chain 1 Property 1: Property Set In Action Chain 1"));
assertTrue("ActionChain2 property should be present",
pageAsText.contains("Action Chain 2 Property 1: Property Set in Action Chain 2"));
assertTrue("ActionChain3 property should be present",
pageAsText.contains("Action Chain 3 Property 1: Property set in Action Chain 3"));
}
}
}
@@ -93,9 +93,16 @@ public interface ActionProxy {
String getMethod();
/**
* Gets status of the method value's initialization.
* Returns whether the action method was explicitly specified rather than defaulting to {@code "execute"}.
* <p>
* This returns {@code true} when the method was provided via the URL (DMI), passed as a constructor argument,
* or resolved from the action configuration (including wildcard-substituted values like {@code method="{1}"}).
* It returns {@code false} only when no method was specified anywhere and the framework fell back
* to the default {@code "execute"} method.
* </p>
*
* @return true if the method returned by getMethod() is not a default initializer value.
* @return {@code true} if the method was explicitly provided or resolved from config;
* {@code false} only when defaulting to {@code "execute"}
*/
boolean isMethodSpecified();
@@ -71,14 +71,14 @@ public class DefaultActionProxy implements ActionProxy, Serializable {
* <p>
* The reason for the builder methods is so that you can use a subclass to create your own DefaultActionProxy instance
* </p>
*
* <p>
* (like a RMIActionProxy).
*
* @param inv the action invocation
* @param namespace the namespace
* @param actionName the action name
* @param methodName the method name
* @param executeResult execute result
* @param inv the action invocation
* @param namespace the namespace
* @param actionName the action name
* @param methodName the method name
* @param executeResult execute result
* @param cleanupContext cleanup context
*/
protected DefaultActionProxy(ActionInvocation inv, String namespace, String actionName, String methodName, boolean executeResult, boolean cleanupContext) {
@@ -171,8 +171,8 @@ public class DefaultActionProxy implements ActionProxy, Serializable {
this.method = config.getMethodName();
if (StringUtils.isEmpty(this.method)) {
this.method = ActionConfig.DEFAULT_METHOD;
methodSpecified = false;
}
methodSpecified = false;
}
}
@@ -522,6 +522,35 @@ public final class StrutsConstants {
*/
public static final String STRUTS_OGNL_EXPRESSION_CACHE_MAXSIZE = "struts.ognl.expressionCacheMaxSize";
/**
* Specifies the type of cache to use for proxy detection. Valid values defined in
* {@link org.apache.struts2.ognl.OgnlCacheFactory.CacheType}.
*
* @since 7.2.0
*/
public static final String STRUTS_PROXY_CACHE_TYPE = "struts.proxy.cacheType";
/**
* Specifies the maximum cache size for proxy detection caches.
*
* @since 7.2.0
*/
public static final String STRUTS_PROXY_CACHE_MAXSIZE = "struts.proxy.cacheMaxSize";
/**
* The {@link org.apache.struts2.ognl.ProxyCacheFactory} implementation class.
*
* @since 7.2.0
*/
public static final String STRUTS_PROXY_CACHE_FACTORY = "struts.proxy.cacheFactory";
/**
* The {@link org.apache.struts2.util.ProxyService} implementation class.
*
* @since 7.2.0
*/
public static final String STRUTS_PROXYSERVICE = "struts.proxyService";
/**
* Enables evaluation of OGNL expressions
*
@@ -707,6 +736,15 @@ public final class StrutsConstants {
*/
public static final String STRUTS_UI_CHECKBOX_SUBMIT_UNCHECKED = "struts.ui.checkbox.submitUnchecked";
/**
* The prefix used for hidden checkbox fields to track unchecked values.
* Default is "__checkbox_" for backward compatibility.
* Set to "struts_checkbox_" to avoid HTML validation warnings about double underscores.
*
* @since 7.2.0
*/
public static final String STRUTS_UI_CHECKBOX_HIDDEN_PREFIX = "struts.ui.checkbox.hiddenPrefix";
/**
* See {@link org.apache.struts2.interceptor.exec.ExecutorProvider}
*/
@@ -49,17 +49,19 @@ import jakarta.servlet.http.HttpServletResponse;
* </pre>
*/
@StrutsTag(
name = "checkbox",
tldTagClass = "org.apache.struts2.views.jsp.ui.CheckboxTag",
description = "Render a checkbox input field",
allowDynamicAttributes = true)
name = "checkbox",
tldTagClass = "org.apache.struts2.views.jsp.ui.CheckboxTag",
description = "Render a checkbox input field",
allowDynamicAttributes = true)
public class Checkbox extends UIBean {
private static final String ATTR_SUBMIT_UNCHECKED = "submitUnchecked";
private static final String ATTR_HIDDEN_PREFIX = "hiddenPrefix";
public static final String TEMPLATE = "checkbox";
private String submitUncheckedGlobal;
private String hiddenPrefixGlobal = "__checkbox_";
protected String fieldValue;
protected String submitUnchecked;
@@ -87,6 +89,8 @@ public class Checkbox extends UIBean {
} else {
addParameter(ATTR_SUBMIT_UNCHECKED, false);
}
addParameter(ATTR_HIDDEN_PREFIX, hiddenPrefixGlobal);
}
@Override
@@ -99,14 +103,19 @@ public class Checkbox extends UIBean {
this.submitUncheckedGlobal = submitUncheckedGlobal;
}
@Inject(value = StrutsConstants.STRUTS_UI_CHECKBOX_HIDDEN_PREFIX, required = false)
public void setHiddenPrefixGlobal(String hiddenPrefixGlobal) {
this.hiddenPrefixGlobal = hiddenPrefixGlobal;
}
@StrutsTagAttribute(description = "The actual HTML value attribute of the checkbox.", defaultValue = "true")
public void setFieldValue(String fieldValue) {
this.fieldValue = fieldValue;
}
@StrutsTagAttribute(description = "If set to true, unchecked elements will be submitted with the form. " +
"Since Struts 6.1.1 you can use a constant \"" + StrutsConstants.STRUTS_UI_CHECKBOX_SUBMIT_UNCHECKED + "\" to set this attribute globally",
type = "Boolean", defaultValue = "false")
"Since Struts 6.1.1 you can use a constant \"" + StrutsConstants.STRUTS_UI_CHECKBOX_SUBMIT_UNCHECKED + "\" to set this attribute globally",
type = "Boolean", defaultValue = "false")
public void setSubmitUnchecked(String submitUnchecked) {
this.submitUnchecked = submitUnchecked;
}
@@ -68,6 +68,14 @@ public class Component {
*/
protected static ConcurrentMap<Class<?>, Collection<String>> standardAttributesMap = new ConcurrentHashMap<>();
/**
* Clears the standard attributes cache to prevent classloader memory leaks during hot redeployment.
* The cache uses Class keys which pin the webapp classloader.
*/
public static void clearStandardAttributesMap() {
standardAttributesMap.clear();
}
protected boolean devMode = false;
protected boolean escapeHtmlBody = false;
protected ValueStack stack;
@@ -61,6 +61,7 @@ import org.apache.struts2.interceptor.exec.ExecutorProvider;
import org.apache.struts2.ognl.BeanInfoCacheFactory;
import org.apache.struts2.ognl.ExpressionCacheFactory;
import org.apache.struts2.ognl.OgnlGuard;
import org.apache.struts2.ognl.ProxyCacheFactory;
import org.apache.struts2.ognl.SecurityMemberAccess;
import org.apache.struts2.ognl.accessor.RootAccessor;
import org.apache.struts2.security.AcceptedPatternsChecker;
@@ -72,6 +73,7 @@ import org.apache.struts2.url.UrlDecoder;
import org.apache.struts2.url.UrlEncoder;
import org.apache.struts2.util.ContentTypeMatcher;
import org.apache.struts2.util.PatternMatcher;
import org.apache.struts2.util.ProxyService;
import org.apache.struts2.util.TextParser;
import org.apache.struts2.util.ValueStackFactory;
import org.apache.struts2.util.location.LocatableProperties;
@@ -442,6 +444,8 @@ public class StrutsBeanSelectionProvider extends AbstractBeanSelectionProvider {
alias(ExpressionCacheFactory.class, StrutsConstants.STRUTS_OGNL_EXPRESSION_CACHE_FACTORY, builder, props, Scope.SINGLETON);
alias(BeanInfoCacheFactory.class, StrutsConstants.STRUTS_OGNL_BEANINFO_CACHE_FACTORY, builder, props, Scope.SINGLETON);
alias(ProxyCacheFactory.class, StrutsConstants.STRUTS_PROXY_CACHE_FACTORY, builder, props, Scope.SINGLETON);
alias(ProxyService.class, StrutsConstants.STRUTS_PROXYSERVICE, builder, props, Scope.SINGLETON);
alias(SecurityMemberAccess.class, StrutsConstants.STRUTS_MEMBER_ACCESS, builder, props, Scope.PROTOTYPE);
alias(OgnlGuard.class, StrutsConstants.STRUTS_OGNL_GUARD, builder, props, Scope.SINGLETON);
@@ -85,13 +85,17 @@ import org.apache.struts2.ognl.ExpressionCacheFactory;
import org.apache.struts2.ognl.OgnlCacheFactory;
import org.apache.struts2.ognl.OgnlReflectionProvider;
import org.apache.struts2.ognl.OgnlUtil;
import org.apache.struts2.ognl.ProxyCacheFactory;
import org.apache.struts2.ognl.StrutsProxyCacheFactory;
import org.apache.struts2.ognl.OgnlValueStackFactory;
import org.apache.struts2.ognl.SecurityMemberAccess;
import org.apache.struts2.ognl.accessor.CompoundRootAccessor;
import org.apache.struts2.ognl.accessor.RootAccessor;
import org.apache.struts2.ognl.accessor.XWorkMethodAccessor;
import org.apache.struts2.util.StrutsProxyService;
import org.apache.struts2.util.OgnlTextParser;
import org.apache.struts2.util.PatternMatcher;
import org.apache.struts2.util.ProxyService;
import org.apache.struts2.text.StrutsLocalizedTextProvider;
import org.apache.struts2.util.TextParser;
import org.apache.struts2.util.ValueStack;
@@ -144,6 +148,8 @@ public class DefaultConfiguration implements Configuration {
constants.put(StrutsConstants.STRUTS_OGNL_EXPRESSION_CACHE_MAXSIZE, 10000);
constants.put(StrutsConstants.STRUTS_OGNL_BEANINFO_CACHE_TYPE, OgnlCacheFactory.CacheType.BASIC);
constants.put(StrutsConstants.STRUTS_OGNL_BEANINFO_CACHE_MAXSIZE, 10000);
constants.put(StrutsConstants.STRUTS_PROXY_CACHE_TYPE, OgnlCacheFactory.CacheType.BASIC);
constants.put(StrutsConstants.STRUTS_PROXY_CACHE_MAXSIZE, 10000);
constants.put(StrutsConstants.STRUTS_ENABLE_DYNAMIC_METHOD_INVOCATION, Boolean.FALSE);
BOOTSTRAP_CONSTANTS = Collections.unmodifiableMap(constants);
}
@@ -243,6 +249,10 @@ public class DefaultConfiguration implements Configuration {
public void destroy() {
packageContexts.clear();
loadedFileNames.clear();
if (container != null) {
container.destroy();
container = null;
}
}
@Override
@@ -260,8 +270,7 @@ public class DefaultConfiguration implements Configuration {
*/
@Override
public synchronized List<PackageProvider> reloadContainer(List<ContainerProvider> providers) throws ConfigurationException {
packageContexts.clear();
loadedFileNames.clear();
destroy();
List<PackageProvider> packageProviders = new ArrayList<>();
ContainerProperties props = new ContainerProperties();
@@ -395,6 +404,8 @@ public class DefaultConfiguration implements Configuration {
.factory(ExpressionCacheFactory.class, DefaultOgnlExpressionCacheFactory.class, Scope.SINGLETON)
.factory(BeanInfoCacheFactory.class, DefaultOgnlBeanInfoCacheFactory.class, Scope.SINGLETON)
.factory(ProxyCacheFactory.class, StrutsProxyCacheFactory.class, Scope.SINGLETON)
.factory(ProxyService.class, StrutsProxyService.class, Scope.SINGLETON)
.factory(OgnlUtil.class, Scope.SINGLETON)
.factory(SecurityMemberAccess.class, Scope.PROTOTYPE)
.factory(OgnlGuard.class, StrutsOgnlGuard.class, Scope.SINGLETON)
@@ -601,26 +612,39 @@ public class DefaultConfiguration implements Configuration {
}
private ActionConfig findActionConfigInNamespace(String namespace, String name) {
ActionConfig config = null;
if (namespace == null) {
namespace = "";
}
Map<String, ActionConfig> actions = namespaceActionConfigs.get(namespace);
if (actions != null) {
config = actions.get(name);
// Check wildcards
if (config == null) {
config = namespaceActionConfigMatchers.get(namespace).match(name);
// fail over to default action
if (config == null) {
String defaultActionRef = namespaceConfigs.get(namespace);
if (defaultActionRef != null) {
config = actions.get(defaultActionRef);
}
}
}
if (actions == null) {
return null;
}
return config;
ActionConfig config = actions.get(name);
if (config != null) {
return config;
}
config = namespaceActionConfigMatchers.get(namespace).match(name);
if (config != null) {
return config;
}
return findDefaultActionConfig(namespace, actions);
}
private ActionConfig findDefaultActionConfig(String namespace, Map<String, ActionConfig> actions) {
String defaultActionRef = namespaceConfigs.get(namespace);
if (defaultActionRef == null) {
return null;
}
ActionConfig config = actions.get(defaultActionRef);
if (config != null) {
return config;
}
return namespaceActionConfigMatchers.get(namespace).match(defaultActionRef);
}
/**
@@ -0,0 +1,35 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.struts2.dispatcher;
import org.apache.struts2.components.Component;
/**
* Clears {@link Component}'s static standard attributes cache to prevent
* classloader leaks on hot redeploy.
*
* @since 7.2.0
*/
public class ComponentCacheDestroyable implements InternalDestroyable {
@Override
public void destroy() {
Component.clearStandardAttributesMap();
}
}
@@ -20,28 +20,65 @@ package org.apache.struts2.dispatcher;
import org.apache.struts2.inject.Container;
import java.util.concurrent.atomic.AtomicLong;
/**
* Simple class to hold Container instance per thread to minimise number of attempts
* to read configuration and build each time a new configuration.
* Per-thread cache for the Container instance, minimising repeated reads from
* {@link org.apache.struts2.config.ConfigurationManager}.
* <p>
* As ContainerHolder operates just per thread (which means per request) there is no need
* to check if configuration changed during the same request. If changed between requests,
* first call to store Container in ContainerHolder will be with the new configuration.
* WW-5537: Uses a ThreadLocal for per-request isolation with an AtomicLong generation
* counter for cross-thread invalidation during app undeploy. When
* {@link #invalidateAll()} is called, all threads see the updated generation on their
* next {@link #get()} and return {@code null}, forcing a fresh read from
* ConfigurationManager. This prevents classloader leaks caused by idle pool threads
* retaining stale Container references after hot redeployment.
*/
class ContainerHolder {
private static final ThreadLocal<Container> instance = new ThreadLocal<>();
private static final ThreadLocal<CachedContainer> instance = new ThreadLocal<>();
/**
* Incremented on each {@link #invalidateAll()} call. Threads compare their cached
* generation against this value to detect staleness.
*/
private static final AtomicLong generation = new AtomicLong();
public static void store(Container newInstance) {
instance.set(newInstance);
instance.set(new CachedContainer(newInstance, generation.get()));
}
public static Container get() {
return instance.get();
CachedContainer cached = instance.get();
if (cached == null) {
return null;
}
if (cached.generation() != generation.get()) {
instance.remove();
return null;
}
return cached.container();
}
/**
* Clears the current thread's cached container reference.
* Used for per-request cleanup.
*/
public static void clear() {
instance.remove();
}
/**
* Invalidates all threads' cached container references by advancing the generation
* counter. Each thread will detect the stale generation on its next {@link #get()}
* call and clear its own ThreadLocal. Also clears the calling thread immediately.
* <p>
* Used during application undeploy ({@link Dispatcher#cleanup()}) to ensure idle
* pool threads do not pin the webapp classloader via retained Container references.
*/
public static void invalidateAll() {
generation.incrementAndGet();
instance.remove();
}
private record CachedContainer(Container container, long generation) {}
}
@@ -0,0 +1,53 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.struts2.dispatcher;
import jakarta.servlet.ServletContext;
/**
* Extension of {@link InternalDestroyable} for components that require
* {@link ServletContext} during cleanup (e.g. clearing servlet-scoped caches).
*
* &lt;p&gt;During {@link Dispatcher#cleanup()}, the discovery loop checks each
* {@code InternalDestroyable} bean: if it implements this subinterface,
* {@link #destroy(ServletContext)} is called instead of {@link #destroy()}.&lt;/p&gt;
*
* @since 7.2.0
* @see InternalDestroyable
* @see Dispatcher#cleanup()
*/
public interface ContextAwareDestroyable extends InternalDestroyable {
/**
* Releases state that requires access to the {@link ServletContext}.
*
* @param servletContext the current servlet context, may be {@code null}
* if the Dispatcher was created without one
*/
void destroy(ServletContext servletContext);
/**
* Default no-op — {@link Dispatcher} calls
* {@link #destroy(ServletContext)} instead when it recognises this type.
*/
@Override
default void destroy() {
// no-op: context-aware variant is the real entry point
}
}
@@ -0,0 +1,35 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.struts2.dispatcher;
import org.apache.struts2.util.DebugUtils;
/**
* Clears {@link DebugUtils}'s static logged-keys cache to prevent memory leaks
* during hot redeployment.
*
* @since 7.2.0
*/
public class DebugUtilsCacheDestroyable implements InternalDestroyable {
@Override
public void destroy() {
DebugUtils.clearCache();
}
}
@@ -441,37 +441,78 @@ public class Dispatcher {
* Releases all instances bound to this dispatcher instance.
*/
public void cleanup() {
// clean up ObjectFactory
if (objectFactory == null) {
LOG.warn("Object Factory is null, something is seriously wrong, no clean up will be performed");
}
if (objectFactory instanceof ObjectFactoryDestroyable) {
try {
((ObjectFactoryDestroyable) objectFactory).destroy();
} catch (Exception e) {
// catch any exception that may occur during destroy() and log it
LOG.error("Exception occurred while destroying ObjectFactory [{}]", objectFactory.toString(), e);
}
}
destroyObjectFactory();
// clean up Dispatcher itself for this thread
instance.remove();
servletContext.setAttribute(StrutsStatics.SERVLET_DISPATCHER, null);
// clean up DispatcherListeners
destroyDispatcherListeners();
destroyInterceptors();
destroyInternalBeans();
// WW-5537: Invalidate all threads' cached Container references to prevent
// classloader leaks from idle pool threads retaining stale references after undeploy.
ContainerHolder.invalidateAll();
//cleanup action context
ActionContext.clear();
// clean up configuration
configurationManager.destroyConfiguration();
configurationManager = null;
}
/**
* Destroys the {@link ObjectFactory} if it implements {@link ObjectFactoryDestroyable}.
*
* @since 7.2.0
*/
protected void destroyObjectFactory() {
if (objectFactory == null) {
LOG.warn("Object Factory is null, something is seriously wrong, no clean up will be performed");
return;
}
if (objectFactory instanceof ObjectFactoryDestroyable ofd) {
try {
ofd.destroy();
} catch (Exception e) {
LOG.error("Exception occurred while destroying ObjectFactory [{}]", objectFactory.toString(), e);
}
}
}
/**
* Notifies all registered {@link DispatcherListener}s that this dispatcher
* is being destroyed, then clears the listener list.
*
* @since 7.2.0
*/
protected void destroyDispatcherListeners() {
if (!dispatcherListeners.isEmpty()) {
for (DispatcherListener l : dispatcherListeners) {
l.dispatcherDestroyed(this);
}
// WW-5537: Clear the static listener list to release references that may
// pin the webapp classloader after undeploy.
dispatcherListeners.clear();
}
}
// clean up all interceptors by calling their destroy() method
/**
* Destroys all interceptors registered in the current configuration.
*
* @since 7.2.0
*/
protected void destroyInterceptors() {
Set<Interceptor> interceptors = new HashSet<>();
Collection<PackageConfig> packageConfigs = configurationManager.getConfiguration().getPackageConfigs().values();
for (PackageConfig packageConfig : packageConfigs) {
for (Object config : packageConfig.getAllInterceptorConfigs().values()) {
if (config instanceof InterceptorStackConfig) {
for (InterceptorMapping interceptorMapping : ((InterceptorStackConfig) config).getInterceptors()) {
if (config instanceof InterceptorStackConfig isc) {
for (InterceptorMapping interceptorMapping : isc.getInterceptors()) {
interceptors.add(interceptorMapping.getInterceptor());
}
}
@@ -480,16 +521,38 @@ public class Dispatcher {
for (Interceptor interceptor : interceptors) {
interceptor.destroy();
}
}
// Clear container holder when application is unloaded / server shutdown
ContainerHolder.clear();
//cleanup action context
ActionContext.clear();
// clean up configuration
configurationManager.destroyConfiguration();
configurationManager = null;
/**
* Discovers and invokes all {@link InternalDestroyable} beans registered
* in the container, clearing static caches and stopping daemon threads
* to prevent classloader leaks during hot redeployment (WW-5537).
*
* <p>Beans implementing {@link ContextAwareDestroyable} receive the
* {@link jakarta.servlet.ServletContext} via
* {@link ContextAwareDestroyable#destroy(jakarta.servlet.ServletContext)}.</p>
*
* @since 7.2.0
*/
protected void destroyInternalBeans() {
if (configurationManager != null && configurationManager.getConfiguration() != null) {
Container container = configurationManager.getConfiguration().getContainer();
Set<String> destroyableNames = container.getInstanceNames(InternalDestroyable.class);
for (String name : destroyableNames) {
try {
InternalDestroyable destroyable = container.getInstance(InternalDestroyable.class, name);
if (destroyable instanceof ContextAwareDestroyable cad) {
cad.destroy(servletContext);
} else {
destroyable.destroy();
}
} catch (Exception e) {
LOG.warn("Error during internal cleanup [{}]", name, e);
}
}
} else {
LOG.warn("ConfigurationManager is null during cleanup, InternalDestroyable beans will not be invoked");
}
}
private void init_FileManager() throws ClassNotFoundException {
@@ -0,0 +1,35 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.struts2.dispatcher;
import org.apache.struts2.inject.util.FinalizableReferenceQueue;
/**
* Adapter that exposes {@link FinalizableReferenceQueue#stopAndClear()} as an
* {@link InternalDestroyable} bean.
*
* @since 7.2.0
*/
public class FinalizableReferenceQueueDestroyable implements InternalDestroyable {
@Override
public void destroy() {
FinalizableReferenceQueue.stopAndClear();
}
}
@@ -0,0 +1,56 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.struts2.dispatcher;
import freemarker.ext.beans.BeansWrapper;
import freemarker.template.Configuration;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.apache.struts2.views.freemarker.FreemarkerManager;
import jakarta.servlet.ServletContext;
/**
* WW-5537: Clears FreeMarker's template and class introspection caches
* stored in {@link ServletContext} during application undeploy, preventing
* classloader leaks.
*
* @since 7.2.0
*/
public class FreemarkerCacheDestroyable implements ContextAwareDestroyable {
private static final Logger LOG = LogManager.getLogger(FreemarkerCacheDestroyable.class);
@Override
public void destroy(ServletContext servletContext) {
if (servletContext == null) {
return;
}
Object fmConfig = servletContext.getAttribute(FreemarkerManager.CONFIG_SERVLET_CONTEXT_KEY);
if (fmConfig instanceof Configuration cfg) {
cfg.clearTemplateCache();
cfg.clearEncodingMap();
if (cfg.getObjectWrapper() instanceof BeansWrapper bw) {
bw.clearClassIntrospectionCache();
}
servletContext.removeAttribute(FreemarkerManager.CONFIG_SERVLET_CONTEXT_KEY);
LOG.debug("FreeMarker configuration cleaned up");
}
}
}
@@ -0,0 +1,45 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.struts2.dispatcher;
/**
* Internal framework interface for components that hold static state
* (caches, daemon threads, etc.) requiring cleanup during application
* undeploy to prevent classloader leaks.
*
* &lt;p&gt;Implementations are registered as named beans in {@code struts-beans.xml}
* (or plugin descriptors) with type {@code InternalDestroyable}. During
* {@link Dispatcher#cleanup()}, all registered implementations are discovered
* via {@code Container.getInstanceNames(InternalDestroyable.class)} and
* invoked automatically.&lt;/p&gt;
*
* &lt;p&gt;This is not part of the public user API. For user/plugin lifecycle
* callbacks, use {@link DispatcherListener} instead.&lt;/p&gt;
*
* @since 7.2.0
* @see Dispatcher#cleanup()
*/
public interface InternalDestroyable {
/**
* Releases static state held by this component. Called once during
* {@link Dispatcher#cleanup()}.
*/
void destroy();
}
@@ -0,0 +1,38 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.struts2.dispatcher;
import org.apache.struts2.ognl.OgnlUtil;
import java.beans.Introspector;
/**
* Clears OGNL runtime caches and JDK introspection caches that hold
* {@code Class<?>} references, preventing classloader leaks on hot redeploy.
*
* @since 7.2.0
*/
public class OgnlCacheDestroyable implements InternalDestroyable {
@Override
public void destroy() {
OgnlUtil.clearRuntimeCache();
Introspector.flushCaches();
}
}
@@ -77,6 +77,7 @@ public class PrepareOperations {
} finally {
ActionContext.clear();
Dispatcher.clearInstance();
ContainerHolder.clear();
devModeOverride.remove();
}
});
@@ -0,0 +1,35 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.struts2.dispatcher;
import org.apache.struts2.interceptor.ScopeInterceptor;
/**
* Clears {@link ScopeInterceptor}'s static locks map to prevent classloader
* leaks on hot redeploy.
*
* @since 7.2.0
*/
public class ScopeInterceptorCacheDestroyable implements InternalDestroyable {
@Override
public void destroy() {
ScopeInterceptor.clearLocks();
}
}
@@ -215,15 +215,15 @@ public abstract class AbstractMultiPartRequest implements MultiPartRequest {
if (maxSize != null) {
LOG.debug("Applies max size: {} to file upload request", maxSize);
servletFileUpload.setSizeMax(maxSize);
servletFileUpload.setMaxSize(maxSize);
}
if (maxFiles != null) {
LOG.debug("Applies max files number: {} to file upload request", maxFiles);
servletFileUpload.setFileCountMax(maxFiles);
servletFileUpload.setMaxFileCount(maxFiles);
}
if (maxFileSize != null) {
LOG.debug("Applies max size of single file: {} to file upload request", maxFileSize);
servletFileUpload.setFileSizeMax(maxFileSize);
servletFileUpload.setMaxFileSize(maxFileSize);
}
return servletFileUpload;
}
@@ -130,4 +130,13 @@ public interface Container extends Serializable {
* Removes the scope strategy for the current thread.
*/
void removeScopeStrategy();
/**
* Releases all internal resources held by this container, including caches,
* factory maps, and thread-local state. This allows the webapp classloader
* to be garbage collected after hot redeployment.
*
* @since 7.2.0
*/
void destroy();
}
@@ -651,6 +651,25 @@ class ContainerImpl implements Container {
void inject(InternalContext context, Object o);
}
/**
* Clears all internal caches, factory maps, and ThreadLocals to release
* Class references that would otherwise pin the webapp classloader after undeploy.
* <p>
* The {@code injectors} and {@code constructors} ReferenceCache maps hold
* {@code Class<?>} keys and reflection accessor objects ({@code Method},
* {@code Constructor}) whose JDK-generated {@code DelegatingClassLoader}
* instances retain the webapp classloader as their parent.
*
* @since 7.2.0
*/
@Override
public void destroy() {
injectors.clear();
constructors.clear();
localContext.remove();
localScopeStrategy.remove();
}
static class MissingDependencyException extends Exception {
MissingDependencyException(String message) {
super(message);
@@ -18,6 +18,7 @@ package org.apache.struts2.inject.util;
import java.lang.ref.Reference;
import java.lang.ref.ReferenceQueue;
import java.util.concurrent.atomic.AtomicReference;
import java.util.logging.Level;
import java.util.logging.Logger;
@@ -26,11 +27,13 @@ import java.util.logging.Logger;
*
* @author Bob Lee (crazybob@google.com)
*/
class FinalizableReferenceQueue extends ReferenceQueue<Object> {
public class FinalizableReferenceQueue extends ReferenceQueue<Object> {
private static final Logger logger =
Logger.getLogger(FinalizableReferenceQueue.class.getName());
private final AtomicReference<Thread> cleanupThread = new AtomicReference<>();
private FinalizableReferenceQueue() {}
void cleanUp(Reference reference) {
@@ -49,18 +52,39 @@ class FinalizableReferenceQueue extends ReferenceQueue<Object> {
Thread thread = new Thread("FinalizableReferenceQueue") {
@Override
public void run() {
while (true) {
while (!Thread.currentThread().isInterrupted()) {
try {
cleanUp(remove());
} catch (InterruptedException e) { /* ignore */ }
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
break;
}
}
}
};
thread.setDaemon(true);
thread.start();
cleanupThread.set(thread);
}
static ReferenceQueue<Object> instance = createAndStart();
/**
* Stops the background cleanup thread to prevent classloader memory leaks during hot redeployment.
*/
void stop() {
Thread t = cleanupThread.getAndSet(null);
if (t != null) {
t.interrupt();
try {
t.join(5000);
} catch (InterruptedException ignored) {
Thread.currentThread().interrupt();
}
t.setContextClassLoader(null);
}
}
private static final AtomicReference<ReferenceQueue<Object>> instance =
new AtomicReference<>(createAndStart());
static FinalizableReferenceQueue createAndStart() {
FinalizableReferenceQueue queue = new FinalizableReferenceQueue();
@@ -72,6 +96,17 @@ class FinalizableReferenceQueue extends ReferenceQueue<Object> {
* Gets instance.
*/
public static ReferenceQueue<Object> getInstance() {
return instance;
return instance.get();
}
/**
* Stops the cleanup thread and clears the instance to prevent classloader
* memory leaks during hot redeployment.
*/
public static void stopAndClear() {
ReferenceQueue<Object> q = instance.getAndSet(null);
if (q instanceof FinalizableReferenceQueue frq) {
frq.stop();
}
}
}
@@ -27,7 +27,7 @@ import org.apache.struts2.inject.Inject;
import org.apache.struts2.result.ActionChainResult;
import org.apache.struts2.result.Result;
import org.apache.struts2.util.CompoundRoot;
import org.apache.struts2.util.ProxyUtil;
import org.apache.struts2.util.ProxyService;
import org.apache.struts2.util.TextParseUtil;
import org.apache.struts2.util.ValueStack;
import org.apache.struts2.util.reflection.ReflectionProvider;
@@ -96,7 +96,7 @@ import java.util.Map;
* </p>
* <!-- END SNIPPET: extending -->
* <u>Example code:</u>
*
* <p>
* <!-- START SNIPPET: example -->
* <pre>
* &lt;action name="someAction" class="com.examples.SomeAction"&gt;
@@ -114,7 +114,6 @@ import java.util.Map;
* </pre>
* <!-- END SNIPPET: example -->
*
*
* @author mrdon
* @author tm_jee ( tm_jee(at)yahoo.co.uk )
* @see ActionChainResult
@@ -135,12 +134,18 @@ public class ChainingInterceptor extends AbstractInterceptor {
protected Collection<String> includes;
protected ReflectionProvider reflectionProvider;
private ProxyService proxyService;
@Inject
public void setReflectionProvider(ReflectionProvider prov) {
this.reflectionProvider = prov;
}
@Inject
public void setProxyService(ProxyService proxyService) {
this.proxyService = proxyService;
}
@Inject(value = StrutsConstants.STRUTS_CHAINING_COPY_ERRORS, required = false)
public void setCopyErrors(String copyErrors) {
this.copyErrors = "true".equalsIgnoreCase(copyErrors);
@@ -175,8 +180,8 @@ public class ChainingInterceptor extends AbstractInterceptor {
}
Object action = invocation.getAction();
Class<?> editable = null;
if (ProxyUtil.isProxy(action)) {
editable = ProxyUtil.ultimateTargetClass(action);
if (proxyService.isProxy(action)) {
editable = proxyService.ultimateTargetClass(action);
}
reflectionProvider.copy(object, action, ctxMap, prepareExcludes(), includes, editable);
}
@@ -184,7 +189,7 @@ public class ChainingInterceptor extends AbstractInterceptor {
private Collection<String> prepareExcludes() {
Collection<String> localExcludes = excludes;
if (!copyErrors || !copyMessages ||!copyFieldErrors) {
if (!copyErrors || !copyMessages || !copyFieldErrors) {
if (localExcludes == null) {
localExcludes = new HashSet<>();
if (!copyErrors) {
@@ -19,6 +19,8 @@
package org.apache.struts2.interceptor;
import org.apache.struts2.ActionInvocation;
import org.apache.struts2.StrutsConstants;
import org.apache.struts2.inject.Inject;
import org.apache.struts2.interceptor.AbstractInterceptor;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
@@ -39,24 +41,27 @@ import java.util.Set;
* of 'false'.
* </p>
* <!-- END SNIPPET: description -->
*
* <p>
* <!-- START SNIPPET: parameters -->
* <ul>
* <li>setUncheckedValue - The default value of an unchecked box can be overridden by setting the 'uncheckedValue' property.</li>
* </ul>
* <!-- END SNIPPET: parameters -->
*
* <p>
* <!-- START SNIPPET: extending -->
*
* <p>
* <!-- END SNIPPET: extending -->
*/
public class CheckboxInterceptor extends AbstractInterceptor {
/** Auto-generated serialization id */
/**
* Auto-generated serialization id
*/
@Serial
private static final long serialVersionUID = -586878104807229585L;
private String uncheckedValue = Boolean.FALSE.toString();
private String hiddenPrefix = "__checkbox_";
private static final Logger LOG = LogManager.getLogger(CheckboxInterceptor.class);
@@ -68,8 +73,8 @@ public class CheckboxInterceptor extends AbstractInterceptor {
Set<String> checkboxParameters = new HashSet<>();
for (Map.Entry<String, Parameter> parameter : parameters.entrySet()) {
String name = parameter.getKey();
if (name.startsWith("__checkbox_")) {
String checkboxName = name.substring("__checkbox_".length());
if (name.startsWith(hiddenPrefix)) {
String checkboxName = name.substring(hiddenPrefix.length());
Parameter value = parameter.getValue();
checkboxParameters.add(name);
@@ -100,4 +105,16 @@ public class CheckboxInterceptor extends AbstractInterceptor {
public void setUncheckedValue(String uncheckedValue) {
this.uncheckedValue = uncheckedValue;
}
/**
* Sets the prefix used for hidden checkbox fields.
* Default is "__checkbox_" for backward compatibility.
*
* @param hiddenPrefix The prefix to use for hidden checkbox fields
* @since 7.2.0
*/
@Inject(value = StrutsConstants.STRUTS_UI_CHECKBOX_HIDDEN_PREFIX, required = false)
public void setHiddenPrefix(String hiddenPrefix) {
this.hiddenPrefix = hiddenPrefix;
}
}
@@ -26,18 +26,11 @@ import org.apache.struts2.util.TextParseUtil;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.apache.logging.log4j.message.ParameterizedMessage;
import org.apache.struts2.ServletActionContext;
import org.apache.struts2.dispatcher.HttpParameters;
import org.apache.struts2.dispatcher.Parameter;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession;
import java.util.Collections;
import java.util.Enumeration;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
@@ -108,6 +101,10 @@ public class I18nInterceptor extends AbstractInterceptor {
.collect(Collectors.toSet());
}
protected boolean isLocaleSupported(Locale locale) {
return supportedLocale.isEmpty() || supportedLocale.contains(locale);
}
@Inject
public void setLocaleProviderFactory(LocaleProviderFactory localeProviderFactory) {
this.localeProviderFactory = localeProviderFactory;
@@ -219,202 +216,167 @@ public class I18nInterceptor extends AbstractInterceptor {
/**
* Uses to handle reading/storing Locale from/in different locations
*/
protected interface LocaleHandler {
Locale find();
Locale read(ActionInvocation invocation);
Locale store(ActionInvocation invocation, Locale locale);
boolean shouldStore();
@Deprecated(forRemoval = true, since = "7.2.0")
protected interface LocaleHandler extends org.apache.struts2.interceptor.i18n.LocaleHandler {
}
protected class RequestLocaleHandler implements LocaleHandler {
/**
* @deprecated Since 7.2.0, use the top-level handler classes in {@code org.apache.struts2.interceptor.i18n}.
* Scheduled for removal in the next release cycle.
*/
@Deprecated(forRemoval = true, since = "7.2.0")
protected abstract class LocaleHandlerAdapter implements LocaleHandler {
protected ActionInvocation actionInvocation;
protected boolean shouldStore = true;
private final org.apache.struts2.interceptor.i18n.LocaleHandler delegate;
protected RequestLocaleHandler(ActionInvocation invocation) {
actionInvocation = invocation;
}
public Locale find() {
LOG.debug("Searching locale in request under parameter {}", requestOnlyParameterName);
Parameter requestedLocale = findLocaleParameter(actionInvocation, requestOnlyParameterName);
if (requestedLocale.isDefined()) {
return getLocaleFromParam(requestedLocale.getValue());
}
return null;
protected LocaleHandlerAdapter(org.apache.struts2.interceptor.i18n.LocaleHandler delegate) {
this.delegate = delegate;
}
@Override
public Locale store(ActionInvocation invocation, Locale locale) {
return locale;
public Locale find() {
return delegate.find();
}
@Override
public Locale read(ActionInvocation invocation) {
LOG.debug("Searching current Invocation context");
// no overriding locale definition found, stay with current invocation (=browser) locale
Locale locale = invocation.getInvocationContext().getLocale();
if (locale != null) {
LOG.debug("Applied invocation context locale: {}", locale);
}
return locale;
return delegate.read(invocation);
}
@Override
public Locale store(ActionInvocation invocation, Locale locale) {
return delegate.store(invocation, locale);
}
@Override
public boolean shouldStore() {
return shouldStore;
return delegate.shouldStore();
}
}
protected class AcceptLanguageLocaleHandler extends RequestLocaleHandler {
private org.apache.struts2.interceptor.i18n.RequestLocaleHandler createRequestDelegate(ActionInvocation invocation, String requestOnlyParam) {
return new org.apache.struts2.interceptor.i18n.RequestLocaleHandler(invocation, requestOnlyParam) {
@Override
protected Locale getLocaleFromParam(String requestedLocale) {
return I18nInterceptor.this.getLocaleFromParam(requestedLocale);
}
@Override
protected Parameter findLocaleParameter(ActionInvocation inv, String paramName) {
return I18nInterceptor.this.findLocaleParameter(inv, paramName);
}
@Override
protected boolean isLocaleSupported(Locale locale) {
return I18nInterceptor.this.isLocaleSupported(locale);
}
};
}
private org.apache.struts2.interceptor.i18n.AcceptLanguageLocaleHandler createAcceptLanguageDelegate(ActionInvocation invocation) {
return new org.apache.struts2.interceptor.i18n.AcceptLanguageLocaleHandler(
invocation, requestOnlyParameterName, supportedLocale
) {
@Override
protected Locale getLocaleFromParam(String requestedLocale) {
return I18nInterceptor.this.getLocaleFromParam(requestedLocale);
}
@Override
protected Parameter findLocaleParameter(ActionInvocation inv, String paramName) {
return I18nInterceptor.this.findLocaleParameter(inv, paramName);
}
@Override
protected boolean isLocaleSupported(Locale locale) {
return I18nInterceptor.this.isLocaleSupported(locale);
}
};
}
private org.apache.struts2.interceptor.i18n.SessionLocaleHandler createSessionDelegate(ActionInvocation invocation) {
return new org.apache.struts2.interceptor.i18n.SessionLocaleHandler(
invocation, requestOnlyParameterName, supportedLocale, parameterName, attributeName
) {
@Override
protected Locale getLocaleFromParam(String requestedLocale) {
return I18nInterceptor.this.getLocaleFromParam(requestedLocale);
}
@Override
protected Parameter findLocaleParameter(ActionInvocation inv, String paramName) {
return I18nInterceptor.this.findLocaleParameter(inv, paramName);
}
@Override
protected boolean isLocaleSupported(Locale locale) {
return I18nInterceptor.this.isLocaleSupported(locale);
}
};
}
private org.apache.struts2.interceptor.i18n.CookieLocaleHandler createCookieDelegate(ActionInvocation invocation) {
return new org.apache.struts2.interceptor.i18n.CookieLocaleHandler(
invocation, requestOnlyParameterName, supportedLocale, requestCookieParameterName, attributeName
) {
@Override
protected Locale getLocaleFromParam(String requestedLocale) {
return I18nInterceptor.this.getLocaleFromParam(requestedLocale);
}
@Override
protected Parameter findLocaleParameter(ActionInvocation inv, String paramName) {
return I18nInterceptor.this.findLocaleParameter(inv, paramName);
}
@Override
protected boolean isLocaleSupported(Locale locale) {
return I18nInterceptor.this.isLocaleSupported(locale);
}
};
}
/**
* @deprecated Since 7.2.0, use {@link org.apache.struts2.interceptor.i18n.RequestLocaleHandler}.
* Scheduled for removal in the next release cycle.
*/
@Deprecated(forRemoval = true, since = "7.2.0")
protected class RequestLocaleHandler extends LocaleHandlerAdapter {
protected RequestLocaleHandler(ActionInvocation invocation) {
super(createRequestDelegate(invocation, requestOnlyParameterName));
}
}
/**
* @deprecated Since 7.2.0, use {@link org.apache.struts2.interceptor.i18n.AcceptLanguageLocaleHandler}.
* Scheduled for removal in the next release cycle.
*/
@Deprecated(forRemoval = true, since = "7.2.0")
protected class AcceptLanguageLocaleHandler extends LocaleHandlerAdapter {
protected AcceptLanguageLocaleHandler(ActionInvocation invocation) {
super(invocation);
super(createAcceptLanguageDelegate(invocation));
}
@Override
@SuppressWarnings("rawtypes")
public Locale find() {
if (!supportedLocale.isEmpty()) {
Enumeration locales = actionInvocation.getInvocationContext().getServletRequest().getLocales();
while (locales.hasMoreElements()) {
Locale locale = (Locale) locales.nextElement();
if (supportedLocale.contains(locale)) {
return locale;
}
}
}
return super.find();
}
}
protected class SessionLocaleHandler extends AcceptLanguageLocaleHandler {
/**
* @deprecated Since 7.2.0, use {@link org.apache.struts2.interceptor.i18n.SessionLocaleHandler}.
* Scheduled for removal in the next release cycle.
*/
@Deprecated(forRemoval = true, since = "7.2.0")
protected class SessionLocaleHandler extends LocaleHandlerAdapter {
protected SessionLocaleHandler(ActionInvocation invocation) {
super(invocation);
}
@Override
public Locale find() {
Locale requestOnlyLocale = super.find();
if (requestOnlyLocale != null) {
LOG.debug("Found locale under request only param, it won't be stored in session!");
shouldStore = false;
return requestOnlyLocale;
}
LOG.debug("Searching locale in request under parameter {}", parameterName);
Parameter requestedLocale = findLocaleParameter(actionInvocation, parameterName);
if (requestedLocale.isDefined()) {
return getLocaleFromParam(requestedLocale.getValue());
}
return null;
}
@Override
public Locale store(ActionInvocation invocation, Locale locale) {
Map<String, Object> session = invocation.getInvocationContext().getSession();
if (session != null) {
String sessionId = ServletActionContext.getRequest().getSession().getId();
synchronized (sessionId.intern()) {
session.put(attributeName, locale);
}
}
return locale;
}
@Override
public Locale read(ActionInvocation invocation) {
Locale locale = null;
LOG.debug("Checks session for saved locale");
HttpSession session = ServletActionContext.getRequest().getSession(false);
if (session != null) {
String sessionId = session.getId();
synchronized (sessionId.intern()) {
Object sessionLocale = invocation.getInvocationContext().getSession().get(attributeName);
if (sessionLocale instanceof Locale) {
locale = (Locale) sessionLocale;
LOG.debug("Applied session locale: {}", locale);
}
}
}
if (locale == null) {
LOG.debug("No Locale defined in session, fetching from current request and it won't be stored in session!");
shouldStore = false;
locale = super.read(invocation);
} else {
LOG.debug("Found stored Locale {} in session, using it!", locale);
}
return locale;
super(createSessionDelegate(invocation));
}
}
protected class CookieLocaleHandler extends AcceptLanguageLocaleHandler {
/**
* @deprecated Since 7.2.0, use {@link org.apache.struts2.interceptor.i18n.CookieLocaleHandler}.
* Scheduled for removal in the next release cycle.
*/
@Deprecated(forRemoval = true, since = "7.2.0")
protected class CookieLocaleHandler extends LocaleHandlerAdapter {
protected CookieLocaleHandler(ActionInvocation invocation) {
super(invocation);
}
@Override
public Locale find() {
Locale requestOnlySessionLocale = super.find();
if (requestOnlySessionLocale != null) {
shouldStore = false;
return requestOnlySessionLocale;
}
LOG.debug("Searching locale in request under parameter {}", requestCookieParameterName);
Parameter requestedLocale = findLocaleParameter(actionInvocation, requestCookieParameterName);
if (requestedLocale.isDefined()) {
return getLocaleFromParam(requestedLocale.getValue());
}
return null;
}
@Override
public Locale store(ActionInvocation invocation, Locale locale) {
HttpServletResponse response = ServletActionContext.getResponse();
Cookie cookie = new Cookie(attributeName, locale.toString());
cookie.setMaxAge(1209600); // two weeks
response.addCookie(cookie);
return locale;
}
@Override
public Locale read(ActionInvocation invocation) {
Locale locale = null;
Cookie[] cookies = ServletActionContext.getRequest().getCookies();
if (cookies != null) {
for (Cookie cookie : cookies) {
if (attributeName.equals(cookie.getName())) {
locale = getLocaleFromParam(cookie.getValue());
}
}
}
if (locale == null) {
LOG.debug("No Locale defined in cookie, fetching from current request and it won't be stored!");
shouldStore = false;
locale = super.read(invocation);
} else {
LOG.debug("Found stored Locale {} in cookie, using it!", locale);
}
return locale;
super(createCookieDelegate(invocation));
}
}
@@ -263,6 +263,15 @@ public class ScopeInterceptor extends AbstractInterceptor implements PreResultLi
}
}
/**
* Clears the locks map to prevent memory leaks during hot redeployment.
*/
public static void clearLocks() {
synchronized (locks) {
locks.clear();
}
}
protected void after(ActionInvocation invocation, String result) throws Exception {
Map<String, Object> session = ActionContext.getContext().getSession();
if ( session != null) {
@@ -0,0 +1,49 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.struts2.interceptor.i18n;
import org.apache.struts2.ActionInvocation;
import org.apache.struts2.dispatcher.Parameter;
import java.util.Locale;
public abstract class AbstractLocaleHandler implements LocaleHandler {
protected final ActionInvocation actionInvocation;
private boolean shouldStore = true;
protected AbstractLocaleHandler(ActionInvocation invocation) {
this.actionInvocation = invocation;
}
@Override
public boolean shouldStore() {
return shouldStore;
}
protected void disableStore() {
this.shouldStore = false;
}
protected abstract Locale getLocaleFromParam(String requestedLocale);
protected abstract Parameter findLocaleParameter(ActionInvocation invocation, String parameterName);
protected abstract boolean isLocaleSupported(Locale locale);
}
@@ -0,0 +1,81 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.struts2.interceptor.i18n;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.apache.struts2.ActionInvocation;
import org.apache.struts2.dispatcher.Parameter;
import java.util.Locale;
import java.util.Set;
public abstract class AbstractStoredLocaleHandler extends AcceptLanguageLocaleHandler {
private static final Logger LOG = LogManager.getLogger(AbstractStoredLocaleHandler.class);
private final String explicitParameterName;
protected AbstractStoredLocaleHandler(ActionInvocation invocation,
String requestOnlyParameterName,
Set<Locale> supportedLocale,
String explicitParameterName) {
super(invocation, requestOnlyParameterName, supportedLocale);
this.explicitParameterName = explicitParameterName;
}
protected Locale findExplicitLocale() {
LOG.debug("Searching locale in request under parameter {}", explicitParameterName);
Parameter requestedLocale = findLocaleParameter(actionInvocation, explicitParameterName);
if (requestedLocale.isDefined()) {
Locale locale = getLocaleFromParam(requestedLocale.getValue());
if (locale != null && isLocaleSupported(locale)) {
return locale;
}
LOG.debug("Requested locale {} is not supported, ignoring", requestedLocale.getValue());
}
return null;
}
protected Locale findRequestOnlyLocale() {
Locale requestOnlyLocale = findRequestOnlyParamLocale();
if (requestOnlyLocale != null) {
LOG.debug("Found locale under request only param, it won't be stored!");
disableStore();
return requestOnlyLocale;
}
return null;
}
protected Locale normalizeStoredLocale(Locale locale, ActionInvocation invocation) {
if (locale != null && !isLocaleSupported(locale)) {
LOG.debug("Stored locale {} is not in supportedLocale, ignoring", locale);
locale = null;
}
if (locale == null) {
LOG.debug("No Locale defined in storage, fetching from current request and it won't be stored!");
disableStore();
return super.read(invocation);
} else {
LOG.debug("Found stored Locale {}, using it!", locale);
return locale;
}
}
}
@@ -0,0 +1,78 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.struts2.interceptor.i18n;
import org.apache.struts2.ActionInvocation;
import java.util.Enumeration;
import java.util.Locale;
import java.util.Set;
/**
* Resolves locale by first checking the request-only parameter and then falling back
* to the browser's {@code Accept-Language} header.
* <p>
* When a {@code supportedLocale} set is configured, only Accept-Language values present
* in that set are accepted. When the set is empty (the default), the first locale
* advertised by the browser is returned as-is.
*
* @see RequestLocaleHandler
* @see AbstractStoredLocaleHandler
*/
public abstract class AcceptLanguageLocaleHandler extends RequestLocaleHandler {
private final Set<Locale> supportedLocale;
protected AcceptLanguageLocaleHandler(ActionInvocation invocation, String requestOnlyParameterName, Set<Locale> supportedLocale) {
super(invocation, requestOnlyParameterName);
this.supportedLocale = supportedLocale;
}
@Override
public Locale find() {
Locale locale = findRequestOnlyParamLocale();
if (locale != null) {
return locale;
}
return findAcceptLanguageLocale();
}
@Override
public Locale read(ActionInvocation invocation) {
if (!supportedLocale.isEmpty()) {
Locale locale = findAcceptLanguageLocale();
if (locale != null) {
return locale;
}
}
return super.read(invocation);
}
@SuppressWarnings("rawtypes")
protected Locale findAcceptLanguageLocale() {
Enumeration locales = actionInvocation.getInvocationContext().getServletRequest().getLocales();
while (locales.hasMoreElements()) {
Locale acceptLocale = (Locale) locales.nextElement();
if (supportedLocale.isEmpty() || supportedLocale.contains(acceptLocale)) {
return acceptLocale;
}
}
return null;
}
}
@@ -0,0 +1,78 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.struts2.interceptor.i18n;
import org.apache.struts2.ActionInvocation;
import org.apache.struts2.ServletActionContext;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletResponse;
import java.util.Locale;
import java.util.Set;
public abstract class CookieLocaleHandler extends AbstractStoredLocaleHandler {
private final String attributeName;
protected CookieLocaleHandler(ActionInvocation invocation,
String requestOnlyParameterName,
Set<Locale> supportedLocale,
String requestCookieParameterName,
String attributeName) {
super(invocation, requestOnlyParameterName, supportedLocale, requestCookieParameterName);
this.attributeName = attributeName;
}
@Override
public Locale find() {
Locale locale = findExplicitLocale();
if (locale != null) {
return locale;
}
return findRequestOnlyLocale();
}
@Override
public Locale store(ActionInvocation invocation, Locale locale) {
HttpServletResponse response = ServletActionContext.getResponse();
Cookie cookie = new Cookie(attributeName, locale.toString());
cookie.setMaxAge(1209600); // two weeks
response.addCookie(cookie);
return locale;
}
@Override
public Locale read(ActionInvocation invocation) {
Locale locale = null;
Cookie[] cookies = ServletActionContext.getRequest().getCookies();
if (cookies != null) {
for (Cookie cookie : cookies) {
if (attributeName.equals(cookie.getName())) {
locale = getLocaleFromParam(cookie.getValue());
}
}
}
return normalizeStoredLocale(locale, invocation);
}
}
@@ -0,0 +1,63 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.struts2.interceptor.i18n;
import org.apache.struts2.ActionInvocation;
import java.util.Locale;
/**
* Strategy used by {@code I18nInterceptor} to resolve and optionally persist the current request locale.
* <p>
* Implementations encapsulate locale source-specific behavior (request parameters, session, cookies,
* or Accept-Language header), while the interceptor orchestrates the overall lifecycle.
*/
public interface LocaleHandler {
/**
* Looks for an explicit locale override in request-scoped sources.
*
* @return a locale override or {@code null} when no explicit override is present
*/
Locale find();
/**
* Reads locale from persistent/context sources when {@link #find()} did not resolve one.
*
* @param invocation current action invocation
* @return resolved locale or {@code null} when no locale could be resolved
*/
Locale read(ActionInvocation invocation);
/**
* Persists the resolved locale when storage is enabled for the current handler.
*
* @param invocation current action invocation
* @param locale locale to store
* @return the effective locale to apply to the invocation context
*/
Locale store(ActionInvocation invocation, Locale locale);
/**
* Indicates if the locale should be persisted for the current request.
*
* @return {@code true} when {@link #store(ActionInvocation, Locale)} should be invoked
*/
boolean shouldStore();
}
@@ -0,0 +1,88 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.struts2.interceptor.i18n;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.apache.struts2.ActionInvocation;
import org.apache.struts2.dispatcher.Parameter;
import java.util.Locale;
/**
* Resolves locale from a request-only parameter (not persisted to session or cookie).
* <p>
* When a matching request parameter is present and the locale is
* {@linkplain #isLocaleSupported(Locale) supported}, it is applied to the current
* request only; it is never stored for subsequent requests.
*
* @see AcceptLanguageLocaleHandler
* @see AbstractStoredLocaleHandler
*/
public abstract class RequestLocaleHandler extends AbstractLocaleHandler {
private static final Logger LOG = LogManager.getLogger(RequestLocaleHandler.class);
private final String requestOnlyParameterName;
protected RequestLocaleHandler(ActionInvocation invocation, String requestOnlyParameterName) {
super(invocation);
this.requestOnlyParameterName = requestOnlyParameterName;
}
@Override
public Locale find() {
return findRequestOnlyParamLocale();
}
/**
* Looks up the locale from the request-only parameter without any additional fallback.
* Subclasses that add fallback logic (e.g. Accept-Language) can override {@link #find()}
* while stored-locale handlers can call this method directly to skip the fallback.
*/
protected Locale findRequestOnlyParamLocale() {
LOG.debug("Searching locale in request under parameter {}", requestOnlyParameterName);
Parameter requestedLocale = findLocaleParameter(actionInvocation, requestOnlyParameterName);
if (requestedLocale.isDefined()) {
Locale locale = getLocaleFromParam(requestedLocale.getValue());
if (locale != null && isLocaleSupported(locale)) {
return locale;
}
LOG.debug("Requested locale {} is not supported, ignoring", requestedLocale.getValue());
}
return null;
}
@Override
public Locale store(ActionInvocation invocation, Locale locale) {
return locale;
}
@Override
public Locale read(ActionInvocation invocation) {
LOG.debug("Searching current Invocation context");
Locale locale = invocation.getInvocationContext().getLocale();
if (locale != null) {
LOG.debug("Applied invocation context locale: {}", locale);
}
return locale;
}
}
@@ -0,0 +1,90 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.struts2.interceptor.i18n;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.apache.struts2.ActionInvocation;
import org.apache.struts2.ServletActionContext;
import jakarta.servlet.http.HttpSession;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
public abstract class SessionLocaleHandler extends AbstractStoredLocaleHandler {
private static final Logger LOG = LogManager.getLogger(SessionLocaleHandler.class);
private final String attributeName;
protected SessionLocaleHandler(ActionInvocation invocation,
String requestOnlyParameterName,
Set<Locale> supportedLocale,
String parameterName,
String attributeName) {
super(invocation, requestOnlyParameterName, supportedLocale, parameterName);
this.attributeName = attributeName;
}
@Override
public Locale find() {
Locale locale = findExplicitLocale();
if (locale != null) {
return locale;
}
return findRequestOnlyLocale();
}
@Override
public Locale store(ActionInvocation invocation, Locale locale) {
Map<String, Object> session = invocation.getInvocationContext().getSession();
if (session != null) {
String sessionId = ServletActionContext.getRequest().getSession().getId();
synchronized (sessionId.intern()) {
session.put(attributeName, locale);
}
}
return locale;
}
@Override
public Locale read(ActionInvocation invocation) {
Locale locale = null;
LOG.debug("Checks session for saved locale");
HttpSession session = ServletActionContext.getRequest().getSession(false);
if (session != null) {
String sessionId = session.getId();
synchronized (sessionId.intern()) {
Object sessionLocale = invocation.getInvocationContext().getSession().get(attributeName);
if (sessionLocale instanceof Locale) {
locale = (Locale) sessionLocale;
LOG.debug("Applied session locale: {}", locale);
}
}
}
return normalizeStoredLocale(locale, invocation);
}
}
@@ -39,7 +39,7 @@ import org.apache.struts2.security.DefaultAcceptedPatternsChecker;
import org.apache.struts2.security.ExcludedPatternsChecker;
import org.apache.struts2.util.ClearableValueStack;
import org.apache.struts2.util.MemberAccessValueStack;
import org.apache.struts2.util.ProxyUtil;
import org.apache.struts2.util.ProxyService;
import org.apache.struts2.util.TextParseUtil;
import org.apache.struts2.util.ValueStack;
import org.apache.struts2.util.ValueStackFactory;
@@ -95,6 +95,7 @@ public class ParametersInterceptor extends MethodFilterInterceptor {
private ValueStackFactory valueStackFactory;
private OgnlUtil ognlUtil;
protected ThreadAllowlist threadAllowlist;
private ProxyService proxyService;
private ExcludedPatternsChecker excludedPatterns;
private AcceptedPatternsChecker acceptedPatterns;
private Set<Pattern> excludedValuePatterns = null;
@@ -115,6 +116,11 @@ public class ParametersInterceptor extends MethodFilterInterceptor {
this.threadAllowlist = threadAllowlist;
}
@Inject
public void setProxyService(ProxyService proxyService) {
this.proxyService = proxyService;
}
@Inject(StrutsConstants.STRUTS_DEVMODE)
public void setDevMode(String mode) {
this.devMode = BooleanUtils.toBoolean(mode);
@@ -516,8 +522,8 @@ public class ParametersInterceptor extends MethodFilterInterceptor {
}
protected Class<?> ultimateClass(Object action) {
if (ProxyUtil.isProxy(action)) {
return ProxyUtil.ultimateTargetClass(action);
if (proxyService.isProxy(action)) {
return proxyService.ultimateTargetClass(action);
}
return action.getClass();
}
@@ -56,4 +56,9 @@ public class MockContainer implements Container {
}
@Override
public void destroy() {
// no-op in mock
}
}
@@ -18,10 +18,9 @@
*/
package org.apache.struts2.ognl;
import ognl.OgnlContext;
import org.apache.struts2.conversion.NullHandler;
public class OgnlNullHandlerWrapper implements ognl.NullHandler {
public class OgnlNullHandlerWrapper implements ognl.NullHandler<StrutsContext> {
private final NullHandler wrapped;
@@ -30,13 +29,13 @@ public class OgnlNullHandlerWrapper implements ognl.NullHandler {
}
@Override
public Object nullMethodResult(OgnlContext context, Object target,
public Object nullMethodResult(StrutsContext context, Object target,
String methodName, Object[] args) {
return wrapped.nullMethodResult(context, target, methodName, args);
}
@Override
public Object nullPropertyValue(OgnlContext context, Object target, Object property) {
public Object nullPropertyValue(StrutsContext context, Object target, Object property) {
return wrapped.nullPropertyValue(context, target, property);
}
@@ -29,6 +29,7 @@ import ognl.Ognl;
public class OgnlReflectionContextFactory implements ReflectionContextFactory {
@Override
@SuppressWarnings({"rawtypes", "unchecked"})
public OgnlContext createDefaultContext(Object root) {
return Ognl.createDefaultContext(root);
}
@@ -18,7 +18,6 @@
*/
package org.apache.struts2.ognl;
import ognl.OgnlContext;
import org.apache.struts2.conversion.TypeConverter;
import java.lang.reflect.Member;
@@ -26,7 +25,7 @@ import java.lang.reflect.Member;
/**
* Wraps an XWork type conversion class for as an OGNL TypeConverter
*/
public class OgnlTypeConverterWrapper implements ognl.TypeConverter {
public class OgnlTypeConverterWrapper implements ognl.TypeConverter<StrutsContext> {
private final TypeConverter typeConverter;
@@ -38,7 +37,7 @@ public class OgnlTypeConverterWrapper implements ognl.TypeConverter {
}
@Override
public Object convertValue(OgnlContext context, Object target, Member member, String propertyName, Object value, Class<?> toType) {
public Object convertValue(StrutsContext context, Object target, Member member, String propertyName, Object value, Class<?> toType) {
return typeConverter.convertValue(context, target, member, propertyName, value, toType);
}
@@ -61,7 +61,7 @@ public class OgnlUtil {
private final OgnlCache<String, Object> expressionCache;
private final OgnlCache<Class<?>, BeanInfo> beanInfoCache;
private TypeConverter defaultConverter;
private TypeConverter<StrutsContext> defaultConverter;
private final OgnlGuard ognlGuard;
private boolean devMode;
@@ -211,14 +211,14 @@ public class OgnlUtil {
* @return an OgnlContext instance
* @since 7.2.0
*/
private OgnlContext ensureOgnlContext(Map<String, Object> context) {
if (context instanceof OgnlContext ognlContext) {
return ognlContext;
private StrutsContext ensureOgnlContext(Map<String, Object> context) {
if (context instanceof StrutsContext strutsContext) {
return strutsContext;
}
// Create a new OgnlContext and copy the Map contents
OgnlContext ognlContext = createDefaultContext(null);
ognlContext.putAll(context);
return ognlContext;
// Create a new StrutsContext and copy the Map contents
StrutsContext strutsContext = createDefaultContext(null);
strutsContext.putAll(context);
return strutsContext;
}
/**
@@ -247,16 +247,18 @@ public class OgnlUtil {
return;
}
OgnlContext ognlContext = ensureOgnlContext(context);
Object oldRoot = Ognl.getRoot(ognlContext);
Ognl.setRoot(ognlContext, o);
for (Map.Entry<String, ?> entry : props.entrySet()) {
String expression = entry.getKey();
internalSetProperty(expression, entry.getValue(), o, context, throwPropertyExceptions);
StrutsContext strutsContext = ensureOgnlContext(context);
try {
withRoot(strutsContext, o, () -> {
for (Map.Entry<String, ?> entry : props.entrySet()) {
String expression = entry.getKey();
internalSetProperty(expression, entry.getValue(), o, context, throwPropertyExceptions);
}
});
} catch (OgnlException e) {
// Should never happen as internalSetProperty catches OgnlException
throw new IllegalStateException("Unexpected OgnlException in setProperties", e);
}
Ognl.setRoot(ognlContext, oldRoot);
}
/**
@@ -307,14 +309,13 @@ public class OgnlUtil {
* problems setting the property
*/
public void setProperty(String name, Object value, Object o, Map<String, Object> context, boolean throwPropertyExceptions) {
OgnlContext ognlContext = ensureOgnlContext(context);
Object oldRoot = Ognl.getRoot(ognlContext);
Ognl.setRoot(ognlContext, o);
internalSetProperty(name, value, o, context, throwPropertyExceptions);
Ognl.setRoot(ognlContext, oldRoot);
StrutsContext strutsContext = ensureOgnlContext(context);
try {
withRoot(strutsContext, o, () -> internalSetProperty(name, value, o, context, throwPropertyExceptions));
} catch (OgnlException e) {
// Should never happen as internalSetProperty catches OgnlException
throw new IllegalStateException("Unexpected OgnlException in setProperty", e);
}
}
/**
@@ -423,15 +424,18 @@ public class OgnlUtil {
for (TreeValidator validator : treeValidators) {
validator.validate(tree, checkContext);
}
Ognl.setValue(tree, (OgnlContext) context, root, value);
StrutsContext ognlContext = (StrutsContext) context;
withRoot(ognlContext, root, () -> Ognl.setValue(tree, ognlContext, root, value));
}
@SuppressWarnings("unchecked")
private <T> T ognlGet(String expr, Map<String, Object> context, Object root, Class<T> resultType, Map<String, Object> checkContext, TreeValidator... treeValidators) throws OgnlException {
Object tree = toTree(expr);
for (TreeValidator validator : treeValidators) {
validator.validate(tree, checkContext);
}
return (T) Ognl.getValue(tree, (OgnlContext) context, root, resultType);
StrutsContext ognlContext = (StrutsContext) context;
return withRoot(ognlContext, root, () -> (T) Ognl.getValue(tree, ognlContext, root, resultType));
}
private Object toTree(String expr) throws OgnlException {
@@ -544,8 +548,8 @@ public class OgnlUtil {
return;
}
final Map<String, Object> contextFrom = createDefaultContext(from);
final Map<String, Object> contextTo = createDefaultContext(to);
final StrutsContext contextFrom = createDefaultContext(from);
final StrutsContext contextTo = createDefaultContext(to);
PropertyDescriptor[] fromPds;
PropertyDescriptor[] toPds;
@@ -650,7 +654,7 @@ public class OgnlUtil {
*/
public Map<String, Object> getBeanMap(final Object source) throws IntrospectionException, OgnlException {
Map<String, Object> beanMap = new HashMap<>();
final Map<String, Object> sourceMap = createDefaultContext(source);
final StrutsContext sourceMap = createDefaultContext(source);
PropertyDescriptor[] propertyDescriptors = getPropertyDescriptors(source);
for (PropertyDescriptor propertyDescriptor : propertyDescriptors) {
final String propertyName = propertyDescriptor.getDisplayName();
@@ -720,22 +724,75 @@ public class OgnlUtil {
}
}
protected OgnlContext createDefaultContext(Object root) {
protected StrutsContext createDefaultContext(Object root) {
return createDefaultContext(root, null);
}
protected OgnlContext createDefaultContext(Object root, ClassResolver resolver) {
protected StrutsContext createDefaultContext(Object root, ClassResolver<StrutsContext> resolver) {
if (resolver == null) {
resolver = container.getInstance(RootAccessor.class);
if (resolver == null) {
throw new IllegalStateException("Cannot find ClassResolver");
}
}
return Ognl.createDefaultContext(root, container.getInstance(SecurityMemberAccess.class), resolver, defaultConverter);
StrutsContext context = new StrutsContext(
container.getInstance(SecurityMemberAccess.class), resolver, defaultConverter);
context.withRoot(root);
return context;
}
@FunctionalInterface
private interface TreeValidator {
void validate(Object tree, Map<String, Object> context) throws OgnlException;
}
@FunctionalInterface
private interface OgnlAction {
void run() throws OgnlException;
}
@FunctionalInterface
private interface OgnlSupplier<T> {
T get() throws OgnlException;
}
/**
* Executes the given action with the specified root set on the OGNL context,
* restoring the original root afterwards.
*
* @param context the OGNL context
* @param root the root object to set during execution
* @param action the action to execute
* @throws OgnlException if the action throws an OgnlException
*/
private void withRoot(StrutsContext context, Object root, OgnlAction action) throws OgnlException {
Object oldRoot = context.getRoot();
try {
context.withRoot(root);
action.run();
} finally {
context.withRoot(oldRoot);
}
}
/**
* Executes the given supplier with the specified root set on the OGNL context,
* restoring the original root afterwards.
*
* @param context the OGNL context
* @param root the root object to set during execution
* @param supplier the supplier to execute
* @param <T> the return type
* @return the result of the supplier
* @throws OgnlException if the supplier throws an OgnlException
*/
private <T> T withRoot(StrutsContext context, Object root, OgnlSupplier<T> supplier) throws OgnlException {
Object oldRoot = context.getRoot();
try {
context.withRoot(root);
return supplier.get();
} finally {
context.withRoot(oldRoot);
}
}
}
@@ -31,8 +31,6 @@ import org.apache.struts2.util.ValueStack;
import org.apache.struts2.util.reflection.ReflectionContextState;
import ognl.MethodFailedException;
import ognl.NoSuchPropertyException;
import ognl.Ognl;
import ognl.OgnlContext;
import ognl.OgnlException;
import org.apache.commons.lang3.BooleanUtils;
import org.apache.logging.log4j.LogManager;
@@ -68,7 +66,7 @@ public class OgnlValueStack implements Serializable, ValueStack, ClearableValueS
private static final String MAP_IDENTIFIER_KEY = "org.apache.struts2.util.OgnlValueStack.MAP_IDENTIFIER_KEY";
protected CompoundRoot root;
protected transient Map<String, Object> context;
protected transient StrutsContext context;
protected Class defaultType;
protected Map<Object, Object> overrides;
protected transient OgnlUtil ognlUtil;
@@ -121,12 +119,12 @@ public class OgnlValueStack implements Serializable, ValueStack, ClearableValueS
protected void setRoot(XWorkConverter xworkConverter, RootAccessor accessor, CompoundRoot compoundRoot, SecurityMemberAccess securityMemberAccess) {
this.root = compoundRoot;
this.securityMemberAccess = securityMemberAccess;
OgnlContext ognlContext = Ognl.createDefaultContext(this.root, securityMemberAccess, accessor, new OgnlTypeConverterWrapper(xworkConverter));
this.context = ognlContext;
this.context = new StrutsContext(securityMemberAccess, accessor, new OgnlTypeConverterWrapper(xworkConverter));
this.context.withRoot(this.root);
this.converter = xworkConverter;
context.put(VALUE_STACK, this);
ognlContext.setTraceEvaluations(false);
ognlContext.setKeepLastEvaluation(false);
context.setTraceEvaluations(false);
context.setKeepLastEvaluation(false);
}
@Inject(StrutsConstants.STRUTS_DEVMODE)
@@ -508,9 +506,7 @@ public class OgnlValueStack implements Serializable, ValueStack, ClearableValueS
@Override
public void clearContextValues() {
//this is an OGNL ValueStack so the context will be an OgnlContext
//it would be better to make context of type OgnlContext
((OgnlContext) context).getValues().clear();
context.getValues().clear();
}
@Override
@@ -54,6 +54,7 @@ public class OgnlValueStackFactory implements ValueStackFactory {
}
@Inject
@SuppressWarnings({"rawtypes", "unchecked"})
protected void setCompoundRootAccessor(RootAccessor compoundRootAccessor) {
this.compoundRootAccessor = compoundRootAccessor;
OgnlRuntime.setPropertyAccessor(CompoundRoot.class, compoundRootAccessor);
@@ -110,6 +111,7 @@ public class OgnlValueStackFactory implements ValueStackFactory {
* {@link #setMethodAccessor} and can be configured using the extension point
* {@link StrutsConstants#STRUTS_METHOD_ACCESSOR}.
*/
@SuppressWarnings({"rawtypes", "unchecked"})
protected void registerAdditionalMethodAccessors() {
Set<String> names = container.getInstanceNames(MethodAccessor.class);
for (String name : names) {
@@ -145,6 +147,7 @@ public class OgnlValueStackFactory implements ValueStackFactory {
}
}
@SuppressWarnings({"rawtypes", "unchecked"})
protected void registerPropertyAccessors() throws ClassNotFoundException {
Set<String> names = container.getInstanceNames(PropertyAccessor.class);
for (String name : names) {
@@ -0,0 +1,27 @@
/*
* Copyright 2022 Apache Software Foundation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.struts2.ognl;
/**
* A proxy interface to be used with Struts DI mechanism for proxy detection caching.
*
* @param &lt;Key&gt; The type for the cache key entries
* @param &lt;Value&gt; The type for the cache value entries
* @since 7.2.0
*/
public interface ProxyCacheFactory<Key, Value> extends OgnlCacheFactory<Key, Value> {
}
@@ -19,13 +19,12 @@
package org.apache.struts2.ognl;
import ognl.MemberAccess;
import ognl.OgnlContext;
import org.apache.commons.lang3.BooleanUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.apache.struts2.StrutsConstants;
import org.apache.struts2.inject.Inject;
import org.apache.struts2.util.ProxyUtil;
import org.apache.struts2.util.ProxyService;
import java.lang.reflect.AccessibleObject;
import java.lang.reflect.Constructor;
@@ -54,7 +53,7 @@ import static org.apache.struts2.util.DebugUtils.logWarningForFirstOccurrence;
* Allows access decisions to be made on the basis of whether a member is static or not.
* Also blocks or allows access to properties.
*/
public class SecurityMemberAccess implements MemberAccess {
public class SecurityMemberAccess implements MemberAccess<StrutsContext> {
private static final Logger LOG = LogManager.getLogger(SecurityMemberAccess.class);
@@ -76,6 +75,8 @@ public class SecurityMemberAccess implements MemberAccess {
private final ProviderAllowlist providerAllowlist;
private final ThreadAllowlist threadAllowlist;
private ProxyService proxyService;
private boolean allowStaticFieldAccess = true;
private Set<Pattern> excludeProperties = emptySet();
@@ -107,8 +108,13 @@ public class SecurityMemberAccess implements MemberAccess {
this.threadAllowlist = threadAllowlist;
}
@Inject
public void setProxyService(ProxyService proxyService) {
this.proxyService = proxyService;
}
@Override
public Object setup(OgnlContext context, Object target, Member member, String propertyName) {
public Object setup(StrutsContext context, Object target, Member member, String propertyName) {
Object result = null;
if (isAccessible(context, target, member, propertyName)) {
@@ -123,7 +129,7 @@ public class SecurityMemberAccess implements MemberAccess {
}
@Override
public void restore(OgnlContext context, Object target, Member member, String propertyName, Object state) {
public void restore(StrutsContext context, Object target, Member member, String propertyName, Object state) {
if (state == null) {
return;
}
@@ -138,7 +144,7 @@ public class SecurityMemberAccess implements MemberAccess {
}
@Override
public boolean isAccessible(OgnlContext context, Object target, Member member, String propertyName) {
public boolean isAccessible(StrutsContext context, Object target, Member member, String propertyName) {
LOG.debug("Checking access for [target: {}, member: {}, property: {}]", target, member, propertyName);
if (member == null) {
@@ -214,15 +220,15 @@ public class SecurityMemberAccess implements MemberAccess {
Class<?> targetClass = target != null ? target.getClass() : null;
if (!disallowProxyObjectAccess && ProxyUtil.isProxy(target)) {
if (!disallowProxyObjectAccess && proxyService.isProxy(target)) {
// If `disallowProxyObjectAccess` is not set, allow resolving Hibernate entities and Spring proxies to their
// underlying classes/members. This allows the allowlist capability to continue working and still offer
// protection in applications where the developer has accepted the risk of allowing OGNL access to Hibernate
// entities and Spring proxies. This is preferred to having to disable the allowlist capability entirely.
Class<?> newTargetClass = ProxyUtil.ultimateTargetClass(target);
Class<?> newTargetClass = proxyService.ultimateTargetClass(target);
if (newTargetClass != targetClass) {
targetClass = newTargetClass;
member = ProxyUtil.resolveTargetMember(member, newTargetClass);
member = proxyService.resolveTargetMember(member, newTargetClass);
}
}
@@ -312,14 +318,14 @@ public class SecurityMemberAccess implements MemberAccess {
* @return {@code true} if proxy object access is allowed
*/
protected boolean checkProxyObjectAccess(Object target) {
return !(disallowProxyObjectAccess && ProxyUtil.isProxy(target));
return !(disallowProxyObjectAccess && proxyService.isProxy(target));
}
/**
* @return {@code true} if proxy member access is allowed
*/
protected boolean checkProxyMemberAccess(Object target, Member member) {
return !(disallowProxyMemberAccess && ProxyUtil.isProxyMember(member, target));
return !(disallowProxyMemberAccess && proxyService.isProxyMember(member, target));
}
/**
@@ -0,0 +1,53 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.struts2.ognl;
import ognl.ClassResolver;
import ognl.MemberAccess;
import ognl.OgnlContext;
import ognl.TypeConverter;
/**
* Struts-specific OGNL evaluation context. Extends {@link OgnlContext} with the
* self-bounded generic parameter to enable type-safe access in all OGNL interface
* implementations ({@link MemberAccess}, {@link ognl.PropertyAccessor}, etc.).
*
* <p>Phase 1: minimal subclass delegating to super constructors.
* Future phases will promote stringly-typed map entries (e.g. {@code DENY_METHOD_EXECUTION},
* {@code CREATE_NULL_OBJECTS}) to proper typed fields.</p>
*
* @since 7.2.0
*/
public class StrutsContext extends OgnlContext<StrutsContext> {
public StrutsContext(MemberAccess<StrutsContext> memberAccess) {
super(memberAccess);
}
public StrutsContext(MemberAccess<StrutsContext> memberAccess,
ClassResolver<StrutsContext> classResolver) {
super(memberAccess, classResolver);
}
public StrutsContext(MemberAccess<StrutsContext> memberAccess,
ClassResolver<StrutsContext> classResolver,
TypeConverter<StrutsContext> typeConverter) {
super(memberAccess, classResolver, typeConverter);
}
}
@@ -0,0 +1,39 @@
/*
* Copyright 2022 Apache Software Foundation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.struts2.ognl;
import org.apache.commons.lang3.EnumUtils;
import org.apache.struts2.StrutsConstants;
import org.apache.struts2.inject.Inject;
/**
* Struts proxy cache factory implementation.
* Used for creating caches for proxy detection operations.
*
* @param &lt;Key&gt; The type for the cache key entries
* @param &lt;Value&gt; The type for the cache value entries
* @since 7.2.0
*/
public class StrutsProxyCacheFactory<Key, Value> extends DefaultOgnlCacheFactory<Key, Value>
implements ProxyCacheFactory<Key, Value> {
@Inject
public StrutsProxyCacheFactory(
@Inject(value = StrutsConstants.STRUTS_PROXY_CACHE_MAXSIZE) String cacheMaxSize,
@Inject(value = StrutsConstants.STRUTS_PROXY_CACHE_TYPE) String defaultCacheType) {
super(Integer.parseInt(cacheMaxSize), EnumUtils.getEnumIgnoreCase(CacheType.class, defaultCacheType));
}
}
@@ -18,7 +18,6 @@
*/
package org.apache.struts2.ognl;
import ognl.OgnlContext;
import org.apache.struts2.conversion.TypeConverter;
import java.lang.reflect.Member;
@@ -29,19 +28,19 @@ import java.util.Map;
*/
public class XWorkTypeConverterWrapper implements TypeConverter {
private final ognl.TypeConverter typeConverter;
private final ognl.TypeConverter<StrutsContext> typeConverter;
public XWorkTypeConverterWrapper(ognl.TypeConverter conv) {
public XWorkTypeConverterWrapper(ognl.TypeConverter<StrutsContext> conv) {
this.typeConverter = conv;
}
@Override
public Object convertValue(Map context, Object target, Member member, String propertyName, Object value, Class toType) {
// Cast context to OgnlContext for OGNL 3.4.8+ compatibility
OgnlContext ognlContext = (context instanceof OgnlContext oc) ? oc : null;
if (ognlContext == null) {
throw new IllegalArgumentException("Context must be an OgnlContext for OGNL 3.4.8+");
// Cast context to StrutsContext for OGNL 3.5.x compatibility
StrutsContext strutsContext = (context instanceof StrutsContext sc) ? sc : null;
if (strutsContext == null) {
throw new IllegalArgumentException("Context must be a StrutsContext for OGNL 3.5.x+");
}
return typeConverter.convertValue(ognlContext, target, member, propertyName, value, toType);
return typeConverter.convertValue(strutsContext, target, member, propertyName, value, toType);
}
}
@@ -18,6 +18,7 @@
*/
package org.apache.struts2.ognl.accessor;
import org.apache.struts2.dispatcher.InternalDestroyable;
import org.apache.struts2.inject.Inject;
import org.apache.struts2.ognl.OgnlValueStack;
import org.apache.struts2.util.CompoundRoot;
@@ -25,7 +26,6 @@ import org.apache.struts2.util.ValueStack;
import ognl.MethodFailedException;
import ognl.NoSuchPropertyException;
import ognl.Ognl;
import ognl.OgnlContext;
import ognl.OgnlException;
import ognl.OgnlRuntime;
import org.apache.commons.lang3.BooleanUtils;
@@ -33,6 +33,7 @@ import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.apache.struts2.StrutsConstants;
import org.apache.struts2.StrutsException;
import org.apache.struts2.ognl.StrutsContext;
import java.beans.IntrospectionException;
import java.beans.PropertyDescriptor;
@@ -57,13 +58,13 @@ import static org.apache.commons.lang3.BooleanUtils.toBoolean;
* @author Rainer Hermanns
* @version $Revision$
*/
public class CompoundRootAccessor implements RootAccessor {
public class CompoundRootAccessor implements RootAccessor, InternalDestroyable {
/**
* Used by OGNl to generate bytecode
*/
@Override
public String getSourceAccessor(OgnlContext context, Object target, Object index) {
public String getSourceAccessor(StrutsContext context, Object target, Object index) {
return null;
}
@@ -71,7 +72,7 @@ public class CompoundRootAccessor implements RootAccessor {
* Used by OGNl to generate bytecode
*/
@Override
public String getSourceSetter(OgnlContext context, Object target, Object index) {
public String getSourceSetter(StrutsContext context, Object target, Object index) {
return null;
}
@@ -95,7 +96,7 @@ public class CompoundRootAccessor implements RootAccessor {
}
@Override
public void setProperty(OgnlContext context, Object target, Object name, Object value) throws OgnlException {
public void setProperty(StrutsContext context, Object target, Object name, Object value) throws OgnlException {
CompoundRoot root = (CompoundRoot) target;
for (Object o : root) {
@@ -137,7 +138,7 @@ public class CompoundRootAccessor implements RootAccessor {
}
@Override
public Object getProperty(OgnlContext context, Object target, Object name) throws OgnlException {
public Object getProperty(StrutsContext context, Object target, Object name) throws OgnlException {
CompoundRoot root = (CompoundRoot) target;
if (name instanceof Integer index) {
@@ -181,7 +182,7 @@ public class CompoundRootAccessor implements RootAccessor {
}
@Override
public Object callMethod(OgnlContext context, Object target, String name, Object[] objects) throws MethodFailedException {
public Object callMethod(StrutsContext context, Object target, String name, Object[] objects) throws MethodFailedException {
CompoundRoot root = (CompoundRoot) target;
if ("describe".equals(name)) {
@@ -269,12 +270,12 @@ public class CompoundRootAccessor implements RootAccessor {
}
@Override
public Object callStaticMethod(OgnlContext transientVars, Class aClass, String s, Object[] objects) throws MethodFailedException {
public Object callStaticMethod(StrutsContext transientVars, Class aClass, String s, Object[] objects) throws MethodFailedException {
return null;
}
@Override
public Class classForName(String className, OgnlContext context) throws ClassNotFoundException {
public Class classForName(String className, StrutsContext context) throws ClassNotFoundException {
Object root = Ognl.getRoot(context);
if (disallowCustomOgnlMap) {
@@ -322,6 +323,18 @@ public class CompoundRootAccessor implements RootAccessor {
}
public static void clearCache() {
invalidMethods.clear();
}
/**
* @since 7.2.0
*/
@Override
public void destroy() {
clearCache();
}
static class MethodCall {
Class clazz;
String name;
@@ -19,20 +19,20 @@
package org.apache.struts2.ognl.accessor;
import ognl.ObjectPropertyAccessor;
import ognl.OgnlContext;
import ognl.OgnlException;
import org.apache.struts2.dispatcher.HttpParameters;
import org.apache.struts2.ognl.StrutsContext;
public class HttpParametersPropertyAccessor extends ObjectPropertyAccessor {
public class HttpParametersPropertyAccessor extends ObjectPropertyAccessor<StrutsContext> {
@Override
public Object getProperty(OgnlContext context, Object target, Object oname) throws OgnlException {
public Object getProperty(StrutsContext context, Object target, Object oname) throws OgnlException {
HttpParameters parameters = (HttpParameters) target;
return parameters.get(String.valueOf(oname)).getObject();
}
@Override
public void setProperty(OgnlContext context, Object target, Object oname, Object value) throws OgnlException {
public void setProperty(StrutsContext context, Object target, Object oname, Object value) throws OgnlException {
throw new OgnlException("Access to " + target.getClass().getName() + " is read-only!");
}
}
@@ -21,12 +21,12 @@ package org.apache.struts2.ognl.accessor;
import org.apache.struts2.conversion.impl.XWorkConverter;
import org.apache.struts2.util.reflection.ReflectionContextState;
import ognl.ObjectPropertyAccessor;
import ognl.OgnlContext;
import ognl.OgnlException;
import org.apache.struts2.ognl.StrutsContext;
public class ObjectAccessor extends ObjectPropertyAccessor {
public class ObjectAccessor extends ObjectPropertyAccessor<StrutsContext> {
@Override
public Object getProperty(OgnlContext map, Object o, Object o1) throws OgnlException {
public Object getProperty(StrutsContext map, Object o, Object o1) throws OgnlException {
Object obj = super.getProperty(map, o, o1);
map.put(XWorkConverter.LAST_BEAN_CLASS_ACCESSED, o.getClass());
@@ -20,10 +20,10 @@ package org.apache.struts2.ognl.accessor;
import org.apache.struts2.ognl.ObjectProxy;
import org.apache.struts2.util.reflection.ReflectionContextState;
import ognl.OgnlContext;
import ognl.OgnlException;
import ognl.OgnlRuntime;
import ognl.PropertyAccessor;
import org.apache.struts2.ognl.StrutsContext;
/**
* Is able to access (set/get) properties on a given object.
@@ -33,13 +33,13 @@ import ognl.PropertyAccessor;
*
* @author Gabe
*/
public class ObjectProxyPropertyAccessor implements PropertyAccessor {
public class ObjectProxyPropertyAccessor implements PropertyAccessor<StrutsContext> {
/**
* Used by OGNl to generate bytecode
*/
@Override
public String getSourceAccessor(OgnlContext context, Object target, Object index) {
public String getSourceAccessor(StrutsContext context, Object target, Object index) {
return null; //To change body of implemented methods use File | Settings | File Templates.
}
@@ -47,25 +47,27 @@ public class ObjectProxyPropertyAccessor implements PropertyAccessor {
* Used by OGNl to generate bytecode
*/
@Override
public String getSourceSetter(OgnlContext context, Object target, Object index) {
public String getSourceSetter(StrutsContext context, Object target, Object index) {
return null;
}
@Override
public Object getProperty(OgnlContext context, Object target, Object name) throws OgnlException {
@SuppressWarnings({"unchecked", "rawtypes"})
public Object getProperty(StrutsContext context, Object target, Object name) throws OgnlException {
ObjectProxy proxy = (ObjectProxy) target;
setupContext(context, proxy);
return OgnlRuntime.getPropertyAccessor(proxy.getValue().getClass()).getProperty(context, target, name);
return ((PropertyAccessor) OgnlRuntime.getPropertyAccessor(proxy.getValue().getClass())).getProperty(context, target, name);
}
@Override
public void setProperty(OgnlContext context, Object target, Object name, Object value) throws OgnlException {
@SuppressWarnings({"unchecked", "rawtypes"})
public void setProperty(StrutsContext context, Object target, Object name, Object value) throws OgnlException {
ObjectProxy proxy = (ObjectProxy) target;
setupContext(context, proxy);
OgnlRuntime.getPropertyAccessor(proxy.getValue().getClass()).setProperty(context, target, name, value);
((PropertyAccessor) OgnlRuntime.getPropertyAccessor(proxy.getValue().getClass())).setProperty(context, target, name, value);
}
/**
@@ -75,7 +77,7 @@ public class ObjectProxyPropertyAccessor implements PropertyAccessor {
* @param context
* @param proxy
*/
private void setupContext(OgnlContext context, ObjectProxy proxy) {
private void setupContext(StrutsContext context, ObjectProxy proxy) {
ReflectionContextState.setLastBeanClassAccessed(context, proxy.getLastClassAccessed());
ReflectionContextState.setLastBeanPropertyAccessed(context, proxy.getLastPropertyAccessed());
}
@@ -19,14 +19,14 @@
package org.apache.struts2.ognl.accessor;
import ognl.ObjectPropertyAccessor;
import ognl.OgnlContext;
import ognl.OgnlException;
import org.apache.struts2.dispatcher.Parameter;
import org.apache.struts2.ognl.StrutsContext;
public class ParameterPropertyAccessor extends ObjectPropertyAccessor {
public class ParameterPropertyAccessor extends ObjectPropertyAccessor<StrutsContext> {
@Override
public Object getProperty(OgnlContext context, Object target, Object oname) throws OgnlException {
public Object getProperty(StrutsContext context, Object target, Object oname) throws OgnlException {
if (target instanceof Parameter parameter) {
if ("value".equalsIgnoreCase(String.valueOf(oname))) {
throw new OgnlException("Access to " + oname + " is not allowed! Call parameter name directly!");
@@ -37,7 +37,7 @@ public class ParameterPropertyAccessor extends ObjectPropertyAccessor {
}
@Override
public void setProperty(OgnlContext context, Object target, Object oname, Object value) throws OgnlException {
public void setProperty(StrutsContext context, Object target, Object oname, Object value) throws OgnlException {
if (target instanceof Parameter) {
throw new OgnlException("Access to " + target.getClass().getName() + " is read-only!");
} else {
@@ -21,9 +21,10 @@ package org.apache.struts2.ognl.accessor;
import ognl.ClassResolver;
import ognl.MethodAccessor;
import ognl.PropertyAccessor;
import org.apache.struts2.ognl.StrutsContext;
/**
* @since 6.4.0
*/
public interface RootAccessor extends PropertyAccessor, MethodAccessor, ClassResolver {
public interface RootAccessor extends PropertyAccessor<StrutsContext>, MethodAccessor<StrutsContext>, ClassResolver<StrutsContext> {
}
@@ -25,10 +25,10 @@ import org.apache.struts2.inject.Inject;
import org.apache.struts2.ognl.OgnlUtil;
import org.apache.struts2.util.reflection.ReflectionContextState;
import ognl.ObjectPropertyAccessor;
import ognl.OgnlContext;
import ognl.OgnlException;
import ognl.OgnlRuntime;
import ognl.SetPropertyAccessor;
import org.apache.struts2.ognl.StrutsContext;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
@@ -40,7 +40,7 @@ import java.util.Map;
/**
* @author Gabe
*/
public class XWorkCollectionPropertyAccessor extends SetPropertyAccessor {
public class XWorkCollectionPropertyAccessor extends SetPropertyAccessor<StrutsContext> {
private static final Logger LOG = LogManager.getLogger(XWorkCollectionPropertyAccessor.class);
@@ -87,7 +87,7 @@ public class XWorkCollectionPropertyAccessor extends SetPropertyAccessor {
* @see ognl.PropertyAccessor#getProperty(java.util.Map, Object, Object)
*/
@Override
public Object getProperty(OgnlContext context, Object target, Object key) throws OgnlException {
public Object getProperty(StrutsContext context, Object target, Object key) throws OgnlException {
LOG.trace("Entering getProperty()");
//check if it is a generic type property.
@@ -186,7 +186,7 @@ public class XWorkCollectionPropertyAccessor extends SetPropertyAccessor {
* Gets an indexed Map by a given key property with the key being
* the value of the property and the value being the
*/
private Map getSetMap(OgnlContext context, Collection collection, String property) throws OgnlException {
private Map getSetMap(StrutsContext context, Collection collection, String property) throws OgnlException {
LOG.trace("getting set Map");
String path = ReflectionContextState.getCurrentPropertyPath(context);
@@ -211,7 +211,7 @@ public class XWorkCollectionPropertyAccessor extends SetPropertyAccessor {
/*
* gets a bean with the given
*/
public Object getPropertyThroughIteration(OgnlContext context, Collection collection, String property, Object key)
public Object getPropertyThroughIteration(StrutsContext context, Collection collection, String property, Object key)
throws OgnlException {
//TODO
for (Object currTest : collection) {
@@ -224,7 +224,7 @@ public class XWorkCollectionPropertyAccessor extends SetPropertyAccessor {
}
@Override
public void setProperty(OgnlContext context, Object target, Object name, Object value) throws OgnlException {
public void setProperty(StrutsContext context, Object target, Object name, Object value) throws OgnlException {
Class lastClass = (Class) context.get(XWorkConverter.LAST_BEAN_CLASS_ACCESSED);
String lastProperty = (String) context.get(XWorkConverter.LAST_BEAN_PROPERTY_ACCESSED);
Class convertToClass = objectTypeDeterminer.getElementClass(lastClass, lastProperty, name);
@@ -256,7 +256,7 @@ public class XWorkCollectionPropertyAccessor extends SetPropertyAccessor {
super.setProperty(context, target, name, realValue);
}
private Object getRealValue(OgnlContext context, Object value, Class convertToClass) {
private Object getRealValue(StrutsContext context, Object value, Class convertToClass) {
if (value == null || convertToClass == null) {
return value;
}
@@ -20,15 +20,15 @@ package org.apache.struts2.ognl.accessor;
import ognl.EnumerationPropertyAccessor;
import ognl.ObjectPropertyAccessor;
import ognl.OgnlContext;
import ognl.OgnlException;
import org.apache.struts2.ognl.StrutsContext;
public class XWorkEnumerationAccessor extends EnumerationPropertyAccessor {
public class XWorkEnumerationAccessor extends EnumerationPropertyAccessor<StrutsContext> {
private final ObjectPropertyAccessor opa = new ObjectPropertyAccessor();
private final ObjectPropertyAccessor<StrutsContext> opa = new ObjectPropertyAccessor<>();
@Override
public void setProperty(OgnlContext context, Object target, Object name, Object value) throws OgnlException {
public void setProperty(StrutsContext context, Object target, Object name, Object value) throws OgnlException {
opa.setProperty(context, target, name, value);
}
}
@@ -20,15 +20,15 @@ package org.apache.struts2.ognl.accessor;
import ognl.IteratorPropertyAccessor;
import ognl.ObjectPropertyAccessor;
import ognl.OgnlContext;
import ognl.OgnlException;
import org.apache.struts2.ognl.StrutsContext;
public class XWorkIteratorPropertyAccessor extends IteratorPropertyAccessor {
public class XWorkIteratorPropertyAccessor extends IteratorPropertyAccessor<StrutsContext> {
private final ObjectPropertyAccessor opa = new ObjectPropertyAccessor();
private final ObjectPropertyAccessor<StrutsContext> opa = new ObjectPropertyAccessor<>();
@Override
public void setProperty(OgnlContext context, Object target, Object name, Object value) throws OgnlException {
public void setProperty(StrutsContext context, Object target, Object name, Object value) throws OgnlException {
opa.setProperty(context, target, name, value);
}
}
@@ -25,9 +25,9 @@ import org.apache.struts2.inject.Inject;
import org.apache.struts2.ognl.OgnlUtil;
import org.apache.struts2.util.reflection.ReflectionContextState;
import ognl.ListPropertyAccessor;
import ognl.OgnlContext;
import ognl.OgnlException;
import ognl.PropertyAccessor;
import org.apache.struts2.ognl.StrutsContext;
import org.apache.struts2.StrutsConstants;
import org.apache.struts2.StrutsException;
@@ -41,7 +41,7 @@ import java.util.List;
*
* @author Gabriel Zimmerman
*/
public class XWorkListPropertyAccessor extends ListPropertyAccessor {
public class XWorkListPropertyAccessor extends ListPropertyAccessor<StrutsContext> {
private XWorkCollectionPropertyAccessor _sAcc = new XWorkCollectionPropertyAccessor();
@@ -57,7 +57,7 @@ public class XWorkListPropertyAccessor extends ListPropertyAccessor {
}
@Inject("java.util.Collection")
public void setXWorkCollectionPropertyAccessor(PropertyAccessor acc) {
public void setXWorkCollectionPropertyAccessor(PropertyAccessor<StrutsContext> acc) {
this._sAcc = (XWorkCollectionPropertyAccessor) acc;
}
@@ -82,7 +82,7 @@ public class XWorkListPropertyAccessor extends ListPropertyAccessor {
}
@Override
public Object getProperty(OgnlContext context, Object target, Object name) throws OgnlException {
public Object getProperty(StrutsContext context, Object target, Object name) throws OgnlException {
if (ReflectionContextState.isGettingByKeyProperty(context)
|| name.equals(XWorkCollectionPropertyAccessor.KEY_PROPERTY_FOR_CREATION)) {
@@ -137,7 +137,7 @@ public class XWorkListPropertyAccessor extends ListPropertyAccessor {
}
@Override
public void setProperty(OgnlContext context, Object target, Object name, Object value)
public void setProperty(StrutsContext context, Object target, Object name, Object value)
throws OgnlException {
Class lastClass = (Class) context.get(XWorkConverter.LAST_BEAN_CLASS_ACCESSED);
@@ -185,7 +185,7 @@ public class XWorkListPropertyAccessor extends ListPropertyAccessor {
super.setProperty(context, target, name, realValue);
}
private Object getRealValue(OgnlContext context, Object value, Class convertToClass) {
private Object getRealValue(StrutsContext context, Object value, Class convertToClass) {
if (value == null || convertToClass == null) {
return value;
}
@@ -24,8 +24,8 @@ import org.apache.struts2.conversion.impl.XWorkConverter;
import org.apache.struts2.inject.Inject;
import org.apache.struts2.util.reflection.ReflectionContextState;
import ognl.MapPropertyAccessor;
import ognl.OgnlContext;
import ognl.OgnlException;
import org.apache.struts2.ognl.StrutsContext;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
@@ -37,7 +37,7 @@ import java.util.Map;
*
* @author Gabriel Zimmerman
*/
public class XWorkMapPropertyAccessor extends MapPropertyAccessor {
public class XWorkMapPropertyAccessor extends MapPropertyAccessor<StrutsContext> {
private static final Logger LOG = LogManager.getLogger(XWorkMapPropertyAccessor.class);
@@ -63,7 +63,7 @@ public class XWorkMapPropertyAccessor extends MapPropertyAccessor {
}
@Override
public Object getProperty(OgnlContext context, Object target, Object name) throws OgnlException {
public Object getProperty(StrutsContext context, Object target, Object name) throws OgnlException {
LOG.trace("Entering getProperty ({},{},{})", context, target, name);
ReflectionContextState.updateCurrentPropertyPath(context, name);
@@ -123,7 +123,7 @@ public class XWorkMapPropertyAccessor extends MapPropertyAccessor {
}
@Override
public void setProperty(OgnlContext context, Object target, Object name, Object value) throws OgnlException {
public void setProperty(StrutsContext context, Object target, Object name, Object value) throws OgnlException {
LOG.trace("Entering setProperty({},{},{},{})", context, target, name, value);
Object key = getKey(context, name);
@@ -131,7 +131,7 @@ public class XWorkMapPropertyAccessor extends MapPropertyAccessor {
map.put(key, getValue(context, value));
}
private Object getValue(OgnlContext context, Object value) {
private Object getValue(StrutsContext context, Object value) {
Class lastClass = (Class) context.get(XWorkConverter.LAST_BEAN_CLASS_ACCESSED);
String lastProperty = (String) context.get(XWorkConverter.LAST_BEAN_PROPERTY_ACCESSED);
if (lastClass == null || lastProperty == null) {
@@ -144,7 +144,7 @@ public class XWorkMapPropertyAccessor extends MapPropertyAccessor {
return xworkConverter.convertValue(context, value, elementClass);
}
private Object getKey(OgnlContext context, Object name) {
private Object getKey(StrutsContext context, Object name) {
Class lastClass = (Class) context.get(XWorkConverter.LAST_BEAN_CLASS_ACCESSED);
String lastProperty = (String) context.get(XWorkConverter.LAST_BEAN_PROPERTY_ACCESSED);
if (lastClass == null || lastProperty == null) {
@@ -21,9 +21,9 @@ package org.apache.struts2.ognl.accessor;
import org.apache.struts2.util.reflection.ReflectionContextState;
import ognl.MethodFailedException;
import ognl.ObjectMethodAccessor;
import ognl.OgnlContext;
import ognl.OgnlRuntime;
import ognl.PropertyAccessor;
import org.apache.struts2.ognl.StrutsContext;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
@@ -38,12 +38,13 @@ import java.util.Collection;
* @author Patrick Lightbody
* @author tmjee
*/
public class XWorkMethodAccessor extends ObjectMethodAccessor {
public class XWorkMethodAccessor extends ObjectMethodAccessor<StrutsContext> {
private static final Logger LOG = LogManager.getLogger(XWorkMethodAccessor.class);
@Override
public Object callMethod(OgnlContext context, Object object, String string, Object[] objects) throws MethodFailedException {
@SuppressWarnings("unchecked")
public Object callMethod(StrutsContext context, Object object, String string, Object[] objects) throws MethodFailedException {
//Collection property accessing
//this if statement ensures that ognl
@@ -94,7 +95,7 @@ public class XWorkMethodAccessor extends ObjectMethodAccessor {
}
}
private Object callMethodWithDebugInfo(OgnlContext context, Object object, String methodName, Object[] objects) throws MethodFailedException {
private Object callMethodWithDebugInfo(StrutsContext context, Object object, String methodName, Object[] objects) throws MethodFailedException {
try {
return super.callMethod(context, object, methodName, objects);
} catch (MethodFailedException e) {
@@ -109,7 +110,7 @@ public class XWorkMethodAccessor extends ObjectMethodAccessor {
}
@Override
public Object callStaticMethod(OgnlContext context, Class aClass, String string, Object[] objects) throws MethodFailedException {
public Object callStaticMethod(StrutsContext context, Class aClass, String string, Object[] objects) throws MethodFailedException {
boolean e = ReflectionContextState.isDenyMethodExecution(context);
if (!e) {
@@ -119,7 +120,7 @@ public class XWorkMethodAccessor extends ObjectMethodAccessor {
}
}
private Object callStaticMethodWithDebugInfo(OgnlContext context, Class aClass, String methodName,
private Object callStaticMethodWithDebugInfo(StrutsContext context, Class aClass, String methodName,
Object[] objects) throws MethodFailedException {
try {
return super.callStaticMethod(context, aClass, methodName, objects);
@@ -21,15 +21,15 @@ package org.apache.struts2.ognl.accessor;
import org.apache.struts2.conversion.impl.XWorkConverter;
import org.apache.struts2.util.reflection.ReflectionContextState;
import ognl.ObjectPropertyAccessor;
import ognl.OgnlContext;
import ognl.OgnlException;
import org.apache.struts2.ognl.StrutsContext;
/**
* @author Gabe
*/
public class XWorkObjectPropertyAccessor extends ObjectPropertyAccessor {
public class XWorkObjectPropertyAccessor extends ObjectPropertyAccessor<StrutsContext> {
@Override
public Object getProperty(OgnlContext context, Object target, Object oname) throws OgnlException {
public Object getProperty(StrutsContext context, Object target, Object oname) throws OgnlException {
//set the last set objects in the context
//so if the next objects accessed are
//Maps or Collections they can use the information
@@ -32,6 +32,13 @@ public final class DebugUtils {
private static final Set<String> IS_LOGGED = ConcurrentHashMap.newKeySet();
/**
* Clears the logged-keys cache to prevent memory leaks during hot redeployment.
*/
public static void clearCache() {
IS_LOGGED.clear();
}
public static void notifyDeveloperOfError(Logger log, Object action, String message) {
if (action instanceof TextProvider tp) {
message = tp.getText("devmode.notification", "Developer Notification:\n{0}", new String[]{message});
@@ -38,6 +38,7 @@ import org.xml.sax.SAXNotSupportedException;
import org.xml.sax.SAXParseException;
import org.xml.sax.helpers.DefaultHandler;
import javax.xml.XMLConstants;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;
@@ -104,6 +105,7 @@ public class DomHelper {
try {
factory.setFeature("http://xml.org/sax/features/external-general-entities", false);
factory.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
factory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);
} catch (ParserConfigurationException | SAXNotRecognizedException | SAXNotSupportedException e) {
throw new StrutsException("Unable to disable resolving external entities!", e);
}
@@ -0,0 +1,101 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.struts2.util;
import java.lang.reflect.Member;
/**
* Service interface for proxy detection and resolution operations.
* Replaces static {@link ProxyUtil} methods with an injectable service.
*
* @since 7.2.0
*/
public interface ProxyService {
/**
* Determine the ultimate target class of the given instance, traversing
* not only a top-level proxy but any number of nested proxies as well &amp;mdash;
* as long as possible without side effects.
*
* @param candidate the instance to check (might be a proxy)
* @return the ultimate target class (or the plain class of the given
* object as fallback; never {@code null})
*/
Class<?> ultimateTargetClass(Object candidate);
/**
* Check whether the given object is a proxy.
*
* @param object the object to check
* @return true if the object is a Spring AOP or Hibernate proxy
*/
boolean isProxy(Object object);
/**
* Check whether the given member is a proxy member of a proxy object or is a static proxy member.
*
* @param member the member to check
* @param object the object to check
* @return true if the member is a proxy member
*/
boolean isProxyMember(Member member, Object object);
/**
* Check whether the given object is a Hibernate proxy.
*
* @param object the object to check
* @return true if the object is a Hibernate proxy
*/
boolean isHibernateProxy(Object object);
/**
* Check whether the given member is a member of a Hibernate proxy.
*
* @param member the member to check
* @return true if the member is a Hibernate proxy member
*/
boolean isHibernateProxyMember(Member member);
/**
* Get the target instance of the given object if it is a Hibernate proxy object,
* otherwise return the given object.
*
* @param object the object to check
* @return the target instance or the original object
*/
Object getHibernateProxyTarget(Object object);
/**
* Resolve matching member on target class.
*
* @param proxyMember the proxy member
* @param targetClass the target class
* @return matching member on target object if one exists, otherwise the same member
*/
Member resolveTargetMember(Member proxyMember, Class<?> targetClass);
/**
* @param proxyMember the proxy member
* @param target the target object
* @return matching member on target object if one exists, otherwise the same member
* @deprecated since 7.1, use {@link #resolveTargetMember(Member, Class)} instead.
*/
@Deprecated
Member resolveTargetMember(Member proxyMember, Object target);
}
@@ -43,47 +43,63 @@ import static java.lang.reflect.Modifier.isStatic;
/**
* <code>ProxyUtil</code>
* <p>
* Various utility methods dealing with proxies
* Various utility methods dealing with proxies.
* </p>
*
* @deprecated since 7.2, inject {@link ProxyService} instead. This class will be removed in a future version.
*/
@Deprecated(since = "7.2")
public class ProxyUtil {
private static final int CACHE_MAX_SIZE = 10000;
private static final int CACHE_INITIAL_CAPACITY = 256;
private static final OgnlCache<Class<?>, Boolean> isProxyCache = new DefaultOgnlCacheFactory<Class<?>, Boolean>(
CACHE_MAX_SIZE, OgnlCacheFactory.CacheType.WTLFU, CACHE_INITIAL_CAPACITY).buildOgnlCache();
CACHE_MAX_SIZE, OgnlCacheFactory.CacheType.BASIC, CACHE_INITIAL_CAPACITY).buildOgnlCache();
private static final OgnlCache<Member, Boolean> isProxyMemberCache = new DefaultOgnlCacheFactory<Member, Boolean>(
CACHE_MAX_SIZE, OgnlCacheFactory.CacheType.WTLFU, CACHE_INITIAL_CAPACITY).buildOgnlCache();
private static final OgnlCache<Object, Class<?>> targetClassCache = new DefaultOgnlCacheFactory<Object, Class<?>>(
CACHE_MAX_SIZE, OgnlCacheFactory.CacheType.WTLFU, CACHE_INITIAL_CAPACITY).buildOgnlCache();
CACHE_MAX_SIZE, OgnlCacheFactory.CacheType.BASIC, CACHE_INITIAL_CAPACITY).buildOgnlCache();
private static final boolean HIBERNATE_AVAILABLE = isHibernateAvailable();
private static boolean isHibernateAvailable() {
try {
Class.forName("org.hibernate.proxy.HibernateProxy");
return true;
} catch (ClassNotFoundException e) {
return false;
}
}
/**
* Determine the ultimate target class of the given instance, traversing
* not only a top-level proxy but any number of nested proxies as well &mdash;
* as long as possible without side effects.
*
* @param candidate the instance to check (might be a proxy)
* @return the ultimate target class (or the plain class of the given
* object as fallback; never {@code null})
* @deprecated since 7.2, inject {@link ProxyService} instead
*/
@Deprecated(since = "7.2")
public static Class<?> ultimateTargetClass(Object candidate) {
return targetClassCache.computeIfAbsent(candidate, k -> {
Class<?> result = null;
if (isSpringAopProxy(k)) {
result = springUltimateTargetClass(k);
} else if (isHibernateProxy(k)) {
result = getHibernateProxyTarget(k).getClass();
}
if (result == null) {
result = k.getClass();
}
return result;
});
Class<?> result = null;
if (isSpringAopProxy(candidate)) {
result = springUltimateTargetClass(candidate);
} else if (isHibernateProxy(candidate)) {
result = getHibernateProxyTarget(candidate).getClass();
}
if (result == null) {
result = candidate.getClass();
}
return result;
}
/**
* Check whether the given object is a proxy.
*
* @param object the object to check
* @return true if the object is a Spring AOP or Hibernate proxy
* @deprecated since 7.2, inject {@link ProxyService} instead
*/
@Deprecated(since = "7.2")
public static boolean isProxy(Object object) {
if (object == null) return false;
return isProxyCache.computeIfAbsent(object.getClass(),
@@ -92,9 +108,13 @@ public class ProxyUtil {
/**
* Check whether the given member is a proxy member of a proxy object or is a static proxy member.
*
* @param member the member to check
* @param object the object to check
* @return true if the member is a proxy member
* @deprecated since 7.2, inject {@link ProxyService} instead
*/
@Deprecated(since = "7.2")
public static boolean isProxyMember(Member member, Object object) {
if (!isStatic(member.getModifiers()) && !isProxy(object)) {
return false;
@@ -107,10 +127,14 @@ public class ProxyUtil {
* Check whether the given object is a Hibernate proxy.
*
* @param object the object to check
* @return true if the object is a Hibernate proxy
* @deprecated since 7.2, inject {@link ProxyService} instead
*/
@Deprecated(since = "7.2")
public static boolean isHibernateProxy(Object object) {
if (!HIBERNATE_AVAILABLE || object == null) return false;
try {
return object != null && HibernateProxy.class.isAssignableFrom(object.getClass());
return HibernateProxy.class.isAssignableFrom(object.getClass());
} catch (LinkageError ignored) {
return false;
}
@@ -120,8 +144,12 @@ public class ProxyUtil {
* Check whether the given member is a member of a Hibernate proxy.
*
* @param member the member to check
* @return true if the member is a Hibernate proxy member
* @deprecated since 7.2, inject {@link ProxyService} instead
*/
@Deprecated(since = "7.2")
public static boolean isHibernateProxyMember(Member member) {
if (!HIBERNATE_AVAILABLE) return false;
try {
return hasMember(HibernateProxy.class, member);
} catch (LinkageError ignored) {
@@ -133,6 +161,7 @@ public class ProxyUtil {
* Determine the ultimate target class of the given spring bean instance, traversing
* not only a top-level spring proxy but any number of nested spring proxies as well &mdash;
* as long as possible without side effects, that is, just for singleton targets.
*
* @param candidate the instance to check (might be a spring AOP proxy)
* @return the ultimate target class (or the plain class of the given
* object as fallback; never {@code null})
@@ -147,6 +176,7 @@ public class ProxyUtil {
/**
* Check whether the given object is a Spring proxy.
*
* @param object the object to check
*/
private static boolean isSpringAopProxy(Object object) {
@@ -159,6 +189,7 @@ public class ProxyUtil {
/**
* Check whether the given member is a member of a spring proxy.
*
* @param member the member to check
*/
private static boolean isSpringProxyMember(Member member) {
@@ -176,7 +207,8 @@ public class ProxyUtil {
/**
* Check whether the given class has a given member.
* @param clazz the class to check
*
* @param clazz the class to check
* @param member the member to check
*/
private static boolean hasMember(Class<?> clazz, Member member) {
@@ -193,9 +225,15 @@ public class ProxyUtil {
}
/**
* Get the target instance of the given object if it is a Hibernate proxy object.
*
* @param object the object to check
* @return the target instance of the given object if it is a Hibernate proxy object, otherwise the given object
* @deprecated since 7.2, inject {@link ProxyService} instead
*/
@Deprecated(since = "7.2")
public static Object getHibernateProxyTarget(Object object) {
if (!HIBERNATE_AVAILABLE) return object;
try {
return Hibernate.unproxy(object);
} catch (LinkageError ignored) {
@@ -204,9 +242,15 @@ public class ProxyUtil {
}
/**
* Resolve matching member on target object.
*
* @param proxyMember the proxy member
* @param target the target object
* @return matching member on target object if one exists, otherwise the same member
* @deprecated since 7.1, use {@link #resolveTargetMember(Member, Class)} instead.
* Since 7.2, inject {@link ProxyService} instead.
*/
@Deprecated
@Deprecated(since = "7.1")
public static Member resolveTargetMember(Member proxyMember, Object target) {
return resolveTargetMember(proxyMember, target.getClass());
}
@@ -0,0 +1,214 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.struts2.util;
import org.apache.commons.lang3.reflect.ConstructorUtils;
import org.apache.commons.lang3.reflect.FieldUtils;
import org.apache.commons.lang3.reflect.MethodUtils;
import org.apache.struts2.inject.Inject;
import org.apache.struts2.ognl.OgnlCache;
import org.apache.struts2.ognl.ProxyCacheFactory;
import org.hibernate.Hibernate;
import org.hibernate.proxy.HibernateProxy;
import org.springframework.aop.TargetClassAware;
import org.springframework.aop.framework.Advised;
import org.springframework.aop.framework.AopProxyUtils;
import org.springframework.aop.support.AopUtils;
import org.springframework.aop.SpringProxy;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Member;
import java.lang.reflect.Method;
import static java.lang.reflect.Modifier.isPublic;
import static java.lang.reflect.Modifier.isStatic;
/**
* Default implementation of {@link ProxyService}.
* Provides proxy detection and resolution for Spring AOP and Hibernate proxies.
*
* @since 7.2.0
*/
public class StrutsProxyService implements ProxyService {
private static final boolean HIBERNATE_AVAILABLE = isHibernateAvailable();
private static boolean isHibernateAvailable() {
try {
Class.forName("org.hibernate.proxy.HibernateProxy");
return true;
} catch (ClassNotFoundException e) {
return false;
}
}
private final OgnlCache<Class<?>, Boolean> isProxyCache;
private final OgnlCache<Member, Boolean> isProxyMemberCache;
@Inject
@SuppressWarnings("unchecked")
public StrutsProxyService(ProxyCacheFactory<?, ?> proxyCacheFactory) {
this.isProxyCache = (OgnlCache<Class<?>, Boolean>) proxyCacheFactory.buildOgnlCache();
this.isProxyMemberCache = (OgnlCache<Member, Boolean>) proxyCacheFactory.buildOgnlCache();
}
@Override
public Class<?> ultimateTargetClass(Object candidate) {
Class<?> result = null;
if (isSpringAopProxy(candidate)) {
result = springUltimateTargetClass(candidate);
} else if (isHibernateProxy(candidate)) {
result = getHibernateProxyTarget(candidate).getClass();
}
if (result == null) {
result = candidate.getClass();
}
return result;
}
@Override
public boolean isProxy(Object object) {
if (object == null) return false;
return isProxyCache.computeIfAbsent(object.getClass(),
k -> isSpringAopProxy(object) || isHibernateProxy(object));
}
@Override
public boolean isProxyMember(Member member, Object object) {
if (!isStatic(member.getModifiers()) && !isProxy(object)) {
return false;
}
return isProxyMemberCache.computeIfAbsent(member,
k -> isSpringProxyMember(member) || isHibernateProxyMember(member));
}
@Override
public boolean isHibernateProxy(Object object) {
if (!HIBERNATE_AVAILABLE || object == null) {
return false;
}
try {
return HibernateProxy.class.isAssignableFrom(object.getClass());
} catch (LinkageError ignored) {
return false;
}
}
@Override
public boolean isHibernateProxyMember(Member member) {
if (!HIBERNATE_AVAILABLE) {
return false;
}
try {
return hasMember(HibernateProxy.class, member);
} catch (LinkageError ignored) {
return false;
}
}
@Override
public Object getHibernateProxyTarget(Object object) {
if (!HIBERNATE_AVAILABLE) {
return object;
}
try {
return Hibernate.unproxy(object);
} catch (LinkageError ignored) {
return object;
}
}
@Override
public Member resolveTargetMember(Member proxyMember, Class<?> targetClass) {
int mod = proxyMember.getModifiers();
if (proxyMember instanceof Method) {
if (isPublic(mod)) {
return MethodUtils.getMatchingAccessibleMethod(targetClass, proxyMember.getName(), ((Method) proxyMember).getParameterTypes());
} else {
return MethodUtils.getMatchingMethod(targetClass, proxyMember.getName(), ((Method) proxyMember).getParameterTypes());
}
} else if (proxyMember instanceof Field) {
return FieldUtils.getField(targetClass, proxyMember.getName(), isPublic(mod));
} else if (proxyMember instanceof Constructor && isPublic(mod)) {
return ConstructorUtils.getMatchingAccessibleConstructor(targetClass, ((Constructor<?>) proxyMember).getParameterTypes());
}
return proxyMember;
}
@Override
@Deprecated
public Member resolveTargetMember(Member proxyMember, Object target) {
return resolveTargetMember(proxyMember, target.getClass());
}
/**
* Determine the ultimate target class of the given spring bean instance.
*/
private Class<?> springUltimateTargetClass(Object candidate) {
try {
return AopProxyUtils.ultimateTargetClass(candidate);
} catch (LinkageError ignored) {
return candidate.getClass();
}
}
/**
* Check whether the given object is a Spring proxy.
*/
private boolean isSpringAopProxy(Object object) {
try {
return AopUtils.isAopProxy(object);
} catch (LinkageError ignored) {
return false;
}
}
/**
* Check whether the given member is a member of a spring proxy.
*/
private boolean isSpringProxyMember(Member member) {
try {
if (hasMember(Advised.class, member))
return true;
if (hasMember(TargetClassAware.class, member))
return true;
if (hasMember(SpringProxy.class, member))
return true;
} catch (LinkageError ignored) {
}
return false;
}
/**
* Check whether the given class has a given member.
*/
private boolean hasMember(Class<?> clazz, Member member) {
if (member instanceof Method method) {
return null != MethodUtils.getMatchingMethod(clazz, member.getName(), method.getParameterTypes());
}
if (member instanceof Field) {
return null != FieldUtils.getField(clazz, member.getName(), true);
}
if (member instanceof Constructor<?> constructor) {
return null != ConstructorUtils.getMatchingAccessibleConstructor(clazz, constructor.getParameterTypes());
}
return false;
}
}
@@ -19,6 +19,7 @@
package org.apache.struts2.util.fs;
import org.apache.struts2.FileManager;
import org.apache.struts2.dispatcher.InternalDestroyable;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
@@ -40,7 +41,7 @@ import static java.util.Objects.requireNonNullElseGet;
/**
* Default implementation of {@link FileManager}
*/
public class DefaultFileManager implements FileManager {
public class DefaultFileManager implements FileManager, InternalDestroyable {
private static final Logger LOG = LogManager.getLogger(DefaultFileManager.class);
@@ -56,6 +57,19 @@ public class DefaultFileManager implements FileManager {
public DefaultFileManager() {
}
public static void clearCache() {
files.clear();
lazyMonitoredFilesCache.clear();
}
/**
* @since 7.2.0
*/
@Override
public void destroy() {
clearCache();
}
public void setReloadingConfigs(boolean reloadingConfigs) {
if (reloadingConfigs && !this.reloadingConfigs) {
//starting monitoring cached not-monitored files (lazy monitoring on demand because of performance)
@@ -19,6 +19,7 @@
package org.apache.struts2.views.jsp;
import org.apache.struts2.ActionContext;
import org.apache.struts2.ActionInvocation;
import org.apache.struts2.config.ConfigurationException;
import org.apache.struts2.util.ValueStack;
import org.apache.logging.log4j.LogManager;
@@ -44,7 +45,7 @@ public class TagUtils {
if (stack == null) {
LOG.warn("No ValueStack in ActionContext!");
throw new ConfigurationException("Rendering tag out of Action scope, accessing directly JSPs is not recommended! " +
"Please read https://struts.apache.org/security/#never-expose-jsp-files-directly");
"Please read https://struts.apache.org/security/#never-expose-jsp-files-directly");
} else {
LOG.trace("Adds the current PageContext to ActionContext");
stack.getActionContext()
@@ -52,6 +53,13 @@ public class TagUtils {
.with(ATTRIBUTES, new AttributeMap(stack.getContext()));
}
// Check for direct JSP access (stack exists but no action invocation)
ActionInvocation ai = ActionContext.getContext().getActionInvocation();
if (ai == null || ai.getAction() == null) {
LOG.warn("Rendering tag out of Action scope, accessing directly JSPs is not recommended! " +
"Please read https://struts.apache.org/security/#never-expose-jsp-files-directly");
}
return stack;
}
@@ -283,6 +283,18 @@ struts.ognl.beanInfoCacheType=wtlfu
### application-specific needs.
struts.ognl.beanInfoCacheMaxSize=10000
### Specifies the type of cache to use for proxy detection. See StrutsConstants class for further information.
struts.proxy.cacheType=wtlfu
### Specifies the maximum cache size for proxy detection caches.
struts.proxy.cacheMaxSize=10000
### Specifies the ProxyCacheFactory implementation class.
struts.proxy.cacheFactory=struts
### Specifies the ProxyService implementation class.
struts.proxyService=struts
### Indicates if Dispatcher should handle unexpected exceptions by calling sendError()
### or simply rethrow it as a ServletException to allow future processing by other frameworks like Spring Security
struts.handle.exception=true
@@ -319,4 +331,8 @@ struts.url.decoder=strutsUrlDecoder
### Defines source to read nonce value from, possible values are: request, session
struts.csp.nonceSource=session
### Checkbox hidden field prefix
### Default prefix for backward compatibility. Change to "struts_checkbox_" for HTML5 validation.
struts.ui.checkbox.hiddenPrefix=__checkbox_
### END SNIPPET: complete_file
+22
View File
@@ -240,6 +240,10 @@
class="org.apache.struts2.ognl.DefaultOgnlExpressionCacheFactory" scope="singleton"/>
<bean type="org.apache.struts2.ognl.BeanInfoCacheFactory" name="struts"
class="org.apache.struts2.ognl.DefaultOgnlBeanInfoCacheFactory" scope="singleton"/>
<bean type="org.apache.struts2.ognl.ProxyCacheFactory" name="struts"
class="org.apache.struts2.ognl.StrutsProxyCacheFactory" scope="singleton"/>
<bean type="org.apache.struts2.util.ProxyService" name="struts"
class="org.apache.struts2.util.StrutsProxyService" scope="singleton"/>
<bean type="org.apache.struts2.url.QueryStringBuilder" name="strutsQueryStringBuilder"
class="org.apache.struts2.url.StrutsQueryStringBuilder" scope="singleton"/>
@@ -256,4 +260,22 @@
<bean type="org.apache.struts2.interceptor.csp.CspNonceReader" name="struts"
class="org.apache.struts2.interceptor.csp.StrutsCspNonceReader"/>
<!-- WW-5537: InternalDestroyable beans for automatic cleanup during undeploy -->
<bean type="org.apache.struts2.dispatcher.InternalDestroyable" name="componentCache"
class="org.apache.struts2.dispatcher.ComponentCacheDestroyable"/>
<bean type="org.apache.struts2.dispatcher.InternalDestroyable" name="compoundRootAccessor"
class="org.apache.struts2.ognl.accessor.CompoundRootAccessor"/>
<bean type="org.apache.struts2.dispatcher.InternalDestroyable" name="defaultFileManager"
class="org.apache.struts2.util.fs.DefaultFileManager"/>
<bean type="org.apache.struts2.dispatcher.InternalDestroyable" name="scopeInterceptorCache"
class="org.apache.struts2.dispatcher.ScopeInterceptorCacheDestroyable"/>
<bean type="org.apache.struts2.dispatcher.InternalDestroyable" name="ognlCache"
class="org.apache.struts2.dispatcher.OgnlCacheDestroyable"/>
<bean type="org.apache.struts2.dispatcher.InternalDestroyable" name="finalizableReferenceQueue"
class="org.apache.struts2.dispatcher.FinalizableReferenceQueueDestroyable"/>
<bean type="org.apache.struts2.dispatcher.InternalDestroyable" name="freemarkerCache"
class="org.apache.struts2.dispatcher.FreemarkerCacheDestroyable"/>
<bean type="org.apache.struts2.dispatcher.InternalDestroyable" name="debugUtilsCache"
class="org.apache.struts2.dispatcher.DebugUtilsCacheDestroyable"/>
</struts>
@@ -41,7 +41,7 @@
<#include "/${attributes.templateDir}/${attributes.expandTheme}/dynamic-attributes.ftl" /><#rt/>
/><#rt/>
<#if attributes.submitUnchecked!false>
<input type="hidden" id="__checkbox_${attributes.id}" name="__checkbox_${attributes.name}" value="${attributes.fieldValue}"<#rt/>
<input type="hidden" id="${attributes.hiddenPrefix}${attributes.id}" name="${attributes.hiddenPrefix}${attributes.name}" value="${attributes.fieldValue}"<#rt/>
<#if attributes.disabled!false>
disabled="disabled"<#rt/>
</#if>
@@ -40,7 +40,7 @@
<#include "/${attributes.templateDir}/${attributes.expandTheme}/dynamic-attributes.ftl" />
/><#rt/>
<#if attributes.submitUnchecked!false>
<input type="hidden" id="__checkbox_${attributes.id}" name="__checkbox_${attributes.name}" value="${attributes.fieldValue}"<#rt/>
<input type="hidden" id="${attributes.hiddenPrefix}${attributes.id}" name="${attributes.hiddenPrefix}${attributes.name}" value="${attributes.fieldValue}"<#rt/>
<#if attributes.disabled!false>
disabled="disabled"<#rt/>
</#if>
@@ -18,25 +18,67 @@
*/
package org.apache.struts2;
import org.apache.struts2.mock.MockActionInvocation;
import org.apache.struts2.StrutsInternalTestCase;
import org.apache.struts2.config.ConfigurationException;
import org.apache.struts2.config.StrutsXmlConfigurationProvider;
import org.junit.Test;
import org.apache.struts2.mock.MockActionInvocation;
public class DefaultActionProxyTest extends StrutsInternalTestCase {
@Test
public void testThorwExceptionOnNotAllowedMethod() throws Exception {
final String filename = "org/apache/struts2/config/providers/xwork-test-allowed-methods.xml";
loadConfigurationProviders(new StrutsXmlConfigurationProvider(filename));
private static final String CONFIG = "org/apache/struts2/config/providers/xwork-test-allowed-methods.xml";
public void testThrowExceptionOnNotAllowedMethod() {
loadConfigurationProviders(new StrutsXmlConfigurationProvider(CONFIG));
DefaultActionProxy dap = new DefaultActionProxy(new MockActionInvocation(), "strict", "Default", "notAllowed", true, true);
container.inject(dap);
try {
dap.prepare();
fail("Must throw exception!");
} catch (Exception e) {
assertEquals(e.getMessage(), "Method notAllowed for action Default is not allowed!");
} catch (ConfigurationException e) {
assertEquals("Method notAllowed for action Default is not allowed!", e.getMessage());
}
}
public void testMethodSpecifiedWhenPassedExplicitly() {
loadConfigurationProviders(new StrutsXmlConfigurationProvider(CONFIG));
DefaultActionProxy dap = new DefaultActionProxy(new MockActionInvocation(), "default", "Default", "input", true, true);
container.inject(dap);
dap.prepare();
assertTrue("Method should be specified when passed as constructor argument", dap.isMethodSpecified());
assertEquals("input", dap.getMethod());
}
public void testMethodSpecifiedWhenResolvedFromConfig() {
loadConfigurationProviders(new StrutsXmlConfigurationProvider(CONFIG));
// ConfigMethod action has method="onPostOnly" in XML config, no method passed in constructor
DefaultActionProxy dap = new DefaultActionProxy(new MockActionInvocation(), "default", "ConfigMethod", null, true, true);
container.inject(dap);
dap.prepare();
assertTrue("Method should be specified when resolved from action config", dap.isMethodSpecified());
assertEquals("onPostOnly", dap.getMethod());
}
public void testMethodNotSpecifiedWhenDefaultingToExecute() {
loadConfigurationProviders(new StrutsXmlConfigurationProvider(CONFIG));
// NoMethod action has no method in XML config and no method passed in constructor
DefaultActionProxy dap = new DefaultActionProxy(new MockActionInvocation(), "default", "NoMethod", null, true, true);
container.inject(dap);
dap.prepare();
assertFalse("Method should not be specified when defaulting to execute", dap.isMethodSpecified());
assertEquals("execute", dap.getMethod());
}
public void testMethodSpecifiedWithWildcardAction() {
loadConfigurationProviders(new StrutsXmlConfigurationProvider(CONFIG));
// Wild-onPostOnly matches Wild-* with method="{1}" -> resolves to "onPostOnly"
DefaultActionProxy dap = new DefaultActionProxy(new MockActionInvocation(), "default", "Wild-onPostOnly", null, true, true);
container.inject(dap);
dap.prepare();
assertTrue("Method should be specified when resolved from wildcard config", dap.isMethodSpecified());
assertEquals("onPostOnly", dap.getMethod());
}
}
@@ -337,6 +337,38 @@ public class ConfigurationTest extends XWorkTestCase {
}
public void testDefaultActionRefWithWildcard() {
RuntimeConfiguration configuration = configurationManager.getConfiguration().getRuntimeConfiguration();
// "unknown-action" doesn't exist in /wildcard-default, so default-action-ref "movie-input" should be used
// "movie-input" matches wildcard "movie-*", so it should resolve via wildcard matching
ActionConfig config = configuration.getActionConfig("/wildcard-default", "unknown-action");
assertNotNull("Default action ref should resolve via wildcard matching", config);
assertEquals("org.apache.struts2.SimpleAction", config.getClassName());
assertEquals("input", config.getMethodName());
}
public void testDefaultActionRefWithExactMatch() {
RuntimeConfiguration configuration = configurationManager.getConfiguration().getRuntimeConfiguration();
// default-action-ref "home" matches an exact action, so it should resolve without wildcard matching
ActionConfig config = configuration.getActionConfig("/exact-default", "unknown-action");
assertNotNull("Default action ref should resolve via exact match", config);
assertEquals("org.apache.struts2.SimpleAction", config.getClassName());
assertEquals("execute", config.getMethodName());
}
public void testDefaultActionRefWithWildcardNoMatch() {
RuntimeConfiguration configuration = configurationManager.getConfiguration().getRuntimeConfiguration();
// default-action-ref "no-match-anywhere" matches neither an exact action nor wildcard "movie-*"
ActionConfig config = configuration.getActionConfig("/wildcard-default-nomatch", "unknown-action");
assertNull("Should return null when default-action-ref matches neither exact nor wildcard", config);
}
@Override
protected void setUp() throws Exception {
super.setUp();
@@ -42,7 +42,7 @@ public class XmlConfigurationProviderAllowedMethodsTest extends ConfigurationTes
Map actionConfigs = pkg.getActionConfigs();
// assertions
assertEquals(5, actionConfigs.size());
assertEquals(8, actionConfigs.size());
ActionConfig action = (ActionConfig) actionConfigs.get("Default");
assertEquals(1, action.getAllowedMethods().size());
@@ -0,0 +1,78 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.struts2.dispatcher;
import org.apache.struts2.inject.Container;
import org.junit.After;
import org.junit.Test;
import java.util.concurrent.atomic.AtomicReference;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
public class ContainerHolderTest {
@After
public void tearDown() {
ContainerHolder.clear();
}
@Test
public void storeAndGet() {
Container c = mock(Container.class);
ContainerHolder.store(c);
assertThat(ContainerHolder.get()).isSameAs(c);
}
@Test
public void clearRemovesCurrentThread() {
ContainerHolder.store(mock(Container.class));
ContainerHolder.clear();
assertThat(ContainerHolder.get()).isNull();
}
@Test
public void invalidateAllMakesOtherThreadsSeeNull() throws Exception {
Container c = mock(Container.class);
// Another thread stores a container
Thread t = new Thread(() -> ContainerHolder.store(c));
t.start();
t.join();
// Invalidate on main thread
ContainerHolder.invalidateAll();
// Other thread's cached value should now be stale
AtomicReference<Container> otherThreadResult = new AtomicReference<>();
Thread t2 = new Thread(() -> otherThreadResult.set(ContainerHolder.get()));
t2.start();
t2.join();
assertThat(otherThreadResult.get()).isNull();
}
@Test
public void invalidateAllClearsCallingThread() {
ContainerHolder.store(mock(Container.class));
ContainerHolder.invalidateAll();
assertThat(ContainerHolder.get()).isNull();
}
}
@@ -0,0 +1,185 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.struts2.dispatcher;
import org.apache.struts2.ActionContext;
import org.apache.struts2.StrutsJUnit4InternalTestCase;
import org.apache.struts2.components.Component;
import org.apache.struts2.inject.Container;
import org.apache.struts2.ognl.accessor.CompoundRootAccessor;
import org.apache.struts2.util.DebugUtils;
import org.apache.struts2.util.fs.DefaultFileManager;
import org.junit.Test;
import java.lang.reflect.Field;
import java.net.URI;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentMap;
import static java.util.Collections.emptyMap;
import static org.assertj.core.api.Assertions.assertThat;
/**
* WW-5537: Verifies that Dispatcher.cleanup() properly clears all static state
* that could prevent classloader garbage collection during hot redeployment.
*/
public class DispatcherCleanupTest extends StrutsJUnit4InternalTestCase {
@Test
public void cleanupDiscoversAllInternalDestroyableBeans() {
initDispatcher(emptyMap());
Container container = dispatcher.getConfigurationManager().getConfiguration().getContainer();
Set<String> names = container.getInstanceNames(InternalDestroyable.class);
Set<String> expected = new HashSet<>(Arrays.asList(
"componentCache", "compoundRootAccessor", "defaultFileManager",
"scopeInterceptorCache", "ognlCache", "finalizableReferenceQueue",
"freemarkerCache", "debugUtilsCache"
));
assertThat(names).containsAll(expected);
}
@Test
@SuppressWarnings("unchecked")
public void cleanupClearsComponentStandardAttributesMap() throws Exception {
initDispatcher(emptyMap());
Field mapField = Component.class.getDeclaredField("standardAttributesMap");
mapField.setAccessible(true);
ConcurrentMap<Class<?>, Collection<String>> map =
(ConcurrentMap<Class<?>, Collection<String>>) mapField.get(null);
map.put(String.class, new ArrayList<>());
assertThat(map).isNotEmpty();
dispatcher.cleanup();
assertThat(map).isEmpty();
}
@Test
@SuppressWarnings("unchecked")
public void cleanupClearsCompoundRootAccessorCache() throws Exception {
initDispatcher(emptyMap());
Field field = CompoundRootAccessor.class.getDeclaredField("invalidMethods");
field.setAccessible(true);
Map<Object, Boolean> invalidMethods = (Map<Object, Boolean>) field.get(null);
invalidMethods.put("testKey", Boolean.TRUE);
assertThat(invalidMethods).isNotEmpty();
dispatcher.cleanup();
assertThat(invalidMethods).isEmpty();
}
@Test
public void cleanupClearsDefaultFileManagerFilesMap() throws Exception {
initDispatcher(emptyMap());
Field filesField = DefaultFileManager.class.getDeclaredField("files");
filesField.setAccessible(true);
@SuppressWarnings("unchecked")
Map<String, Object> files = (Map<String, Object>) filesField.get(null);
files.put("test-key", new Object());
assertThat(files).isNotEmpty();
dispatcher.cleanup();
assertThat(files).isEmpty();
}
@Test
public void cleanupClearsDefaultFileManagerLazyCache() throws Exception {
initDispatcher(emptyMap());
Field lazyCacheField = DefaultFileManager.class.getDeclaredField("lazyMonitoredFilesCache");
lazyCacheField.setAccessible(true);
@SuppressWarnings("unchecked")
List<URL> lazyCache = (List<URL>) lazyCacheField.get(null);
lazyCache.add(new URI("file:///test").toURL());
assertThat(lazyCache).isNotEmpty();
dispatcher.cleanup();
assertThat(lazyCache).isEmpty();
}
@Test
public void cleanupClearsDispatcherListeners() throws Exception {
initDispatcher(emptyMap());
Dispatcher.addDispatcherListener(new DispatcherListener() {
@Override
public void dispatcherInitialized(Dispatcher du) {
// intentionally empty test only verifies listener list is cleared
}
@Override
public void dispatcherDestroyed(Dispatcher du) {
// intentionally empty test only verifies listener list is cleared
}
});
dispatcher.cleanup();
Field listenersField = Dispatcher.class.getDeclaredField("dispatcherListeners");
listenersField.setAccessible(true);
List<?> listeners = (List<?>) listenersField.get(null);
assertThat(listeners).isEmpty();
}
@Test
public void cleanupClearsThreadLocals() {
assertThat(Dispatcher.getInstance()).isNotNull();
assertThat(ActionContext.getContext()).isNotNull();
dispatcher.cleanup();
assertThat(Dispatcher.getInstance()).isNull();
assertThat(ActionContext.getContext()).isNull();
}
@Test
@SuppressWarnings("unchecked")
public void cleanupClearsDebugUtilsCache() throws Exception {
initDispatcher(emptyMap());
Field field = DebugUtils.class.getDeclaredField("IS_LOGGED");
field.setAccessible(true);
Set<String> isLogged = (Set<String>) field.get(null);
isLogged.add("test-key");
assertThat(isLogged).isNotEmpty();
dispatcher.cleanup();
assertThat(isLogged).isEmpty();
}
}
@@ -38,171 +38,227 @@ public class CheckboxInterceptorTest extends StrutsInternalTestCase {
private Map<String, Object> param;
protected void setUp() throws Exception {
super.setUp();
param = new HashMap<>();
super.setUp();
param = new HashMap<>();
interceptor = new CheckboxInterceptor();
ai = new MockActionInvocation();
ai.setInvocationContext(ActionContext.getContext());
interceptor = new CheckboxInterceptor();
ai = new MockActionInvocation();
ai.setInvocationContext(ActionContext.getContext());
}
private void prepare(ActionInvocation ai) {
ai.getInvocationContext().withParameters(HttpParameters.create(param).build());
}
private void prepare(ActionInvocation ai) {
ai.getInvocationContext().withParameters(HttpParameters.create(param).build());
}
public void testNoParam() throws Exception {
prepare(ai);
public void testNoParam() throws Exception {
prepare(ai);
interceptor.init();
interceptor.intercept(ai);
interceptor.destroy();
interceptor.init();
interceptor.intercept(ai);
interceptor.destroy();
assertEquals(0, param.size());
}
assertEquals(0, param.size());
}
public void testPassthroughOne() throws Exception {
param.put("user", "batman");
public void testPassthroughOne() throws Exception {
param.put("user", "batman");
prepare(ai);
prepare(ai);
interceptor.init();
interceptor.intercept(ai);
interceptor.destroy();
interceptor.init();
interceptor.intercept(ai);
interceptor.destroy();
assertEquals(1, ai.getInvocationContext().getParameters().keySet().size());
}
assertEquals(1, ai.getInvocationContext().getParameters().size());
}
public void testPassthroughTwo() throws Exception {
param.put("user", "batman");
param.put("email", "batman@comic.org");
public void testPassthroughTwo() throws Exception {
param.put("user", "batman");
param.put("email", "batman@comic.org");
prepare(ai);
prepare(ai);
interceptor.init();
interceptor.intercept(ai);
interceptor.destroy();
interceptor.init();
interceptor.intercept(ai);
interceptor.destroy();
assertEquals(2, ai.getInvocationContext().getParameters().keySet().size());
}
assertEquals(2, ai.getInvocationContext().getParameters().size());
}
public void testOneCheckboxTrue() throws Exception {
param.put("user", "batman");
param.put("email", "batman@comic.org");
param.put("superpower", "true");
param.put("__checkbox_superpower", "true");
assertTrue(param.containsKey("__checkbox_superpower"));
public void testOneCheckboxTrue() throws Exception {
param.put("user", "batman");
param.put("email", "batman@comic.org");
param.put("superpower", "true");
param.put("__checkbox_superpower", "true");
assertTrue(param.containsKey("__checkbox_superpower"));
prepare(ai);
prepare(ai);
interceptor.init();
interceptor.intercept(ai);
interceptor.destroy();
interceptor.init();
interceptor.intercept(ai);
interceptor.destroy();
HttpParameters parameters = ai.getInvocationContext().getParameters();
assertFalse(parameters.contains("__checkbox_superpower"));
assertEquals(3, parameters.keySet().size()); // should be 3 as __checkbox_ should be removed
assertEquals("true", parameters.get("superpower").getValue());
}
HttpParameters parameters = ai.getInvocationContext().getParameters();
assertFalse(parameters.contains("__checkbox_superpower"));
assertEquals(3, parameters.size()); // should be 3 as __checkbox_ should be removed
assertEquals("true", parameters.get("superpower").getValue());
}
public void testOneCheckboxNoValue() throws Exception {
param.put("user", "batman");
param.put("email", "batman@comic.org");
param.put("__checkbox_superpower", "false");
assertTrue(param.containsKey("__checkbox_superpower"));
public void testOneCheckboxNoValue() throws Exception {
param.put("user", "batman");
param.put("email", "batman@comic.org");
param.put("__checkbox_superpower", "false");
assertTrue(param.containsKey("__checkbox_superpower"));
prepare(ai);
prepare(ai);
interceptor.init();
interceptor.intercept(ai);
interceptor.destroy();
interceptor.init();
interceptor.intercept(ai);
interceptor.destroy();
HttpParameters parameters = ai.getInvocationContext().getParameters();
assertFalse(parameters.contains("__checkbox_superpower"));
assertEquals(3, parameters.keySet().size()); // should be 3 as __checkbox_ should be removed
assertEquals("false", parameters.get("superpower").getValue());
}
HttpParameters parameters = ai.getInvocationContext().getParameters();
assertFalse(parameters.contains("__checkbox_superpower"));
assertEquals(3, parameters.size()); // should be 3 as __checkbox_ should be removed
assertEquals("false", parameters.get("superpower").getValue());
}
public void testOneCheckboxNoValueDifferentDefault() throws Exception {
param.put("user", "batman");
param.put("email", "batman@comic.org");
param.put("__checkbox_superpower", "false");
assertTrue(param.containsKey("__checkbox_superpower"));
public void testOneCheckboxNoValueDifferentDefault() throws Exception {
param.put("user", "batman");
param.put("email", "batman@comic.org");
param.put("__checkbox_superpower", "false");
assertTrue(param.containsKey("__checkbox_superpower"));
prepare(ai);
prepare(ai);
interceptor.setUncheckedValue("off");
interceptor.init();
interceptor.intercept(ai);
interceptor.destroy();
interceptor.setUncheckedValue("off");
interceptor.init();
interceptor.intercept(ai);
interceptor.destroy();
HttpParameters parameters = ai.getInvocationContext().getParameters();
assertFalse(parameters.contains("__checkbox_superpower"));
assertEquals(3, parameters.keySet().size()); // should be 3 as __checkbox_ should be removed
assertEquals("off", parameters.get("superpower").getValue());
}
HttpParameters parameters = ai.getInvocationContext().getParameters();
assertFalse(parameters.contains("__checkbox_superpower"));
assertEquals(3, parameters.size()); // should be 3 as __checkbox_ should be removed
assertEquals("off", parameters.get("superpower").getValue());
}
public void testTwoCheckboxNoValue() throws Exception {
param.put("user", "batman");
param.put("email", "batman@comic.org");
param.put("__checkbox_superpower", new String[]{"true", "true"});
param.put("user", "batman");
param.put("email", "batman@comic.org");
param.put("__checkbox_superpower", new String[]{"true", "true"});
prepare(ai);
prepare(ai);
interceptor.init();
interceptor.intercept(ai);
interceptor.destroy();
interceptor.init();
interceptor.intercept(ai);
interceptor.destroy();
HttpParameters parameters = ai.getInvocationContext().getParameters();
assertFalse(parameters.contains("__checkbox_superpower"));
assertEquals(2, parameters.keySet().size()); // should be 2 as __checkbox_ should be removed
assertFalse(parameters.get("superpower").isDefined());
HttpParameters parameters = ai.getInvocationContext().getParameters();
assertFalse(parameters.contains("__checkbox_superpower"));
assertEquals(2, parameters.size()); // should be 2 as __checkbox_ should be removed
assertFalse(parameters.get("superpower").isDefined());
}
public void testTwoCheckboxMixed() throws Exception {
param.put("user", "batman");
param.put("email", "batman@comic.org");
param.put("__checkbox_superpower", "true");
param.put("superpower", "yes");
param.put("__checkbox_cool", "no");
assertTrue(param.containsKey("__checkbox_superpower"));
assertTrue(param.containsKey("__checkbox_cool"));
param.put("user", "batman");
param.put("email", "batman@comic.org");
param.put("__checkbox_superpower", "true");
param.put("superpower", "yes");
param.put("__checkbox_cool", "no");
assertTrue(param.containsKey("__checkbox_superpower"));
assertTrue(param.containsKey("__checkbox_cool"));
prepare(ai);
prepare(ai);
interceptor.init();
interceptor.intercept(ai);
interceptor.destroy();
interceptor.init();
interceptor.intercept(ai);
interceptor.destroy();
HttpParameters parameters = ai.getInvocationContext().getParameters();
assertFalse(parameters.contains("__checkbox_superpower"));
assertFalse(parameters.contains("__checkbox_cool"));
assertEquals(4, parameters.keySet().size()); // should be 4 as __checkbox_ should be removed
assertEquals("yes", parameters.get("superpower").getValue());
assertEquals("false", parameters.get("cool").getValue()); // will use false as default and not 'no'
}
HttpParameters parameters = ai.getInvocationContext().getParameters();
assertFalse(parameters.contains("__checkbox_superpower"));
assertFalse(parameters.contains("__checkbox_cool"));
assertEquals(4, parameters.size()); // should be 4 as __checkbox_ should be removed
assertEquals("yes", parameters.get("superpower").getValue());
assertEquals("false", parameters.get("cool").getValue()); // will use false as default and not 'no'
}
public void testTwoCheckboxMixedWithDifferentDefault() throws Exception {
param.put("user", "batman");
param.put("email", "batman@comic.org");
param.put("__checkbox_superpower", "true");
param.put("superpower", "yes");
param.put("__checkbox_cool", "no");
assertTrue(param.containsKey("__checkbox_superpower"));
assertTrue(param.containsKey("__checkbox_cool"));
public void testTwoCheckboxMixedWithDifferentDefault() throws Exception {
param.put("user", "batman");
param.put("email", "batman@comic.org");
param.put("__checkbox_superpower", "true");
param.put("superpower", "yes");
param.put("__checkbox_cool", "no");
assertTrue(param.containsKey("__checkbox_superpower"));
assertTrue(param.containsKey("__checkbox_cool"));
prepare(ai);
prepare(ai);
interceptor.setUncheckedValue("no");
interceptor.init();
interceptor.intercept(ai);
interceptor.destroy();
interceptor.setUncheckedValue("no");
interceptor.init();
interceptor.intercept(ai);
interceptor.destroy();
HttpParameters parameters = ai.getInvocationContext().getParameters();
assertFalse(parameters.contains("__checkbox_superpower"));
assertFalse(parameters.contains("__checkbox_cool"));
assertEquals(4, parameters.keySet().size()); // should be 4 as __checkbox_ should be removed
assertEquals("yes", parameters.get("superpower").getValue());
assertEquals("no", parameters.get("cool").getValue());
}
HttpParameters parameters = ai.getInvocationContext().getParameters();
assertFalse(parameters.contains("__checkbox_superpower"));
assertFalse(parameters.contains("__checkbox_cool"));
assertEquals(4, parameters.size()); // should be 4 as __checkbox_ should be removed
assertEquals("yes", parameters.get("superpower").getValue());
assertEquals("no", parameters.get("cool").getValue());
}
public void testCustomHiddenPrefixChecked() throws Exception {
param.put("user", "batman");
param.put("struts_checkbox_superpower", "true");
param.put("superpower", "yes");
assertTrue(param.containsKey("struts_checkbox_superpower"));
prepare(ai);
interceptor.setHiddenPrefix("struts_checkbox_");
interceptor.init();
interceptor.intercept(ai);
interceptor.destroy();
HttpParameters parameters = ai.getInvocationContext().getParameters();
assertFalse(parameters.contains("struts_checkbox_superpower"));
assertEquals(2, parameters.size());
assertEquals("yes", parameters.get("superpower").getValue());
}
public void testCustomHiddenPrefixUnchecked() throws Exception {
param.put("user", "batman");
param.put("struts_checkbox_superpower", "true");
assertTrue(param.containsKey("struts_checkbox_superpower"));
prepare(ai);
interceptor.setHiddenPrefix("struts_checkbox_");
interceptor.init();
interceptor.intercept(ai);
interceptor.destroy();
HttpParameters parameters = ai.getInvocationContext().getParameters();
assertFalse(parameters.contains("struts_checkbox_superpower"));
assertEquals(2, parameters.size());
assertEquals("false", parameters.get("superpower").getValue());
}
public void testCustomHiddenPrefixIgnoresDefaultPrefix() throws Exception {
param.put("user", "batman");
param.put("__checkbox_superpower", "true");
assertTrue(param.containsKey("__checkbox_superpower"));
prepare(ai);
interceptor.setHiddenPrefix("struts_checkbox_");
interceptor.init();
interceptor.intercept(ai);
interceptor.destroy();
HttpParameters parameters = ai.getInvocationContext().getParameters();
// With custom prefix, the default __checkbox_ prefix should be ignored
assertTrue(parameters.contains("__checkbox_superpower"));
assertEquals(2, parameters.size());
assertFalse(parameters.get("superpower").isDefined());
}
}
@@ -35,6 +35,7 @@ import org.springframework.mock.web.MockHttpSession;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletResponse;
import java.io.Serializable;
import java.util.Arrays;
import java.util.HashMap;
@@ -100,7 +101,7 @@ public class I18nInterceptorTest extends TestCase {
assertFalse(mai.getInvocationContext().getParameters().get(I18nInterceptor.DEFAULT_PARAMETER).isDefined()); // should have been removed
Locale denmark = new Locale("da", "DK");
Locale denmark = new Locale.Builder().setLanguage("da").setRegion("DK").build();
assertNotNull(session.get(I18nInterceptor.DEFAULT_SESSION_ATTRIBUTE)); // should be stored here
assertEquals(denmark, session.get(I18nInterceptor.DEFAULT_SESSION_ATTRIBUTE)); // should create a locale object
}
@@ -111,7 +112,7 @@ public class I18nInterceptorTest extends TestCase {
assertFalse(mai.getInvocationContext().getParameters().get(I18nInterceptor.DEFAULT_PARAMETER).isDefined()); // should have been removed
Locale denmark = new Locale("da", "DK");
Locale denmark = new Locale.Builder().setLanguage("da").setRegion("DK").build();
assertNull(session.get(I18nInterceptor.DEFAULT_SESSION_ATTRIBUTE)); // should be stored here
assertEquals(denmark, mai.getInvocationContext().getLocale()); // should create a locale object
}
@@ -122,7 +123,7 @@ public class I18nInterceptorTest extends TestCase {
assertFalse(mai.getInvocationContext().getParameters().get(I18nInterceptor.DEFAULT_PARAMETER).isDefined()); // should have been removed
Locale denmark = new Locale("da");
Locale denmark = Locale.forLanguageTag("da");
assertNotNull(session.get(I18nInterceptor.DEFAULT_SESSION_ATTRIBUTE)); // should be stored here
assertEquals(denmark, session.get(I18nInterceptor.DEFAULT_SESSION_ATTRIBUTE)); // should create a locale object
}
@@ -173,7 +174,7 @@ public class I18nInterceptorTest extends TestCase {
assertFalse(mai.getInvocationContext().getParameters().get(I18nInterceptor.DEFAULT_PARAMETER).isDefined()); // should have been removed
Locale variant = new Locale("ja", "JP", "JP");
Locale variant = Locale.forLanguageTag("ja-JP-x-lvariant-JP");
Locale locale = (Locale) session.get(I18nInterceptor.DEFAULT_SESSION_ATTRIBUTE);
assertNotNull(locale); // should be stored here
assertEquals(variant, locale);
@@ -187,7 +188,7 @@ public class I18nInterceptorTest extends TestCase {
assertFalse(mai.getInvocationContext().getParameters().get(I18nInterceptor.DEFAULT_PARAMETER).isDefined()); // should have been removed
assertNull(session.get(I18nInterceptor.DEFAULT_SESSION_ATTRIBUTE));
Locale variant = new Locale("ja", "JP", "JP");
Locale variant = Locale.forLanguageTag("ja-JP-x-lvariant-JP");
Locale locale = mai.getInvocationContext().getLocale();
assertNotNull(locale); // should be stored here
assertEquals(variant, locale);
@@ -205,7 +206,7 @@ public class I18nInterceptorTest extends TestCase {
}
public void testRealLocalesInParams() throws Exception {
Locale[] locales = new Locale[] { Locale.CANADA_FRENCH };
Locale[] locales = new Locale[]{Locale.CANADA_FRENCH};
assertTrue(locales.getClass().isArray());
prepare(I18nInterceptor.DEFAULT_PARAMETER, locales);
interceptor.intercept(mai);
@@ -265,7 +266,7 @@ public class I18nInterceptorTest extends TestCase {
public void testAcceptLanguageBasedLocale() throws Exception {
// given
request.setPreferredLocales(Arrays.asList(new Locale("da_DK"), new Locale("pl")));
request.setPreferredLocales(Arrays.asList(Locale.forLanguageTag("da-DK"), Locale.forLanguageTag("pl")));
interceptor.setLocaleStorage(null);
interceptor.setSupportedLocale("en,pl");
@@ -275,12 +276,143 @@ public class I18nInterceptorTest extends TestCase {
// then
assertNull(session.get(I18nInterceptor.DEFAULT_SESSION_ATTRIBUTE)); // should not be stored here
assertNull(session.get(I18nInterceptor.DEFAULT_SESSION_ATTRIBUTE)); // should not create a locale object
assertEquals(new Locale("pl"), mai.getInvocationContext().getLocale());
assertEquals(Locale.forLanguageTag("pl"), mai.getInvocationContext().getLocale());
}
public void testSupportedLocaleWithRequestLocale() throws Exception {
// given - supportedLocale configured + request_locale param with SESSION storage
request.setPreferredLocales(Arrays.asList(Locale.ENGLISH));
interceptor.setSupportedLocale("en,fr");
prepare(I18nInterceptor.DEFAULT_PARAMETER, "fr");
// when
interceptor.intercept(mai);
// then - request_locale wins over Accept-Language
assertEquals(Locale.FRENCH, session.get(I18nInterceptor.DEFAULT_SESSION_ATTRIBUTE));
assertEquals(Locale.FRENCH, mai.getInvocationContext().getLocale());
}
public void testSupportedLocaleRejectsUnsupportedRequestLocale() throws Exception {
// given - request_locale=es but supportedLocale="en,fr"
request.setPreferredLocales(Arrays.asList(Locale.ENGLISH));
interceptor.setSupportedLocale("en,fr");
prepare(I18nInterceptor.DEFAULT_PARAMETER, "es");
// when
interceptor.intercept(mai);
// then - es rejected, falls back to Accept-Language match (en)
assertNull(session.get(I18nInterceptor.DEFAULT_SESSION_ATTRIBUTE));
assertEquals(Locale.ENGLISH, mai.getInvocationContext().getLocale());
}
public void testSupportedLocaleRevalidatesSessionLocale() throws Exception {
// given - session has stored locale "de" but supportedLocale changed to "en,fr"
session.put(I18nInterceptor.DEFAULT_SESSION_ATTRIBUTE, Locale.GERMAN);
request.setPreferredLocales(Arrays.asList(Locale.FRENCH));
interceptor.setSupportedLocale("en,fr");
// when
interceptor.intercept(mai);
// then - stored "de" rejected, falls back to Accept-Language match (fr)
assertEquals(Locale.FRENCH, mai.getInvocationContext().getLocale());
}
public void testSupportedLocaleWithCookieStorage() throws Exception {
// given - supportedLocale configured + request_cookie_locale param with COOKIE storage
prepare(I18nInterceptor.DEFAULT_COOKIE_PARAMETER, "fr");
request.setPreferredLocales(Arrays.asList(Locale.ENGLISH));
interceptor.setSupportedLocale("en,fr");
final Cookie cookie = new Cookie(I18nInterceptor.DEFAULT_COOKIE_ATTRIBUTE, "fr");
HttpServletResponse response = EasyMock.createMock(HttpServletResponse.class);
response.addCookie(CookieMatcher.eqCookie(cookie));
EasyMock.replay(response);
ac.put(StrutsStatics.HTTP_RESPONSE, response);
interceptor.setLocaleStorage(I18nInterceptor.Storage.COOKIE.name());
// when
interceptor.intercept(mai);
// then - request_cookie_locale=fr wins
EasyMock.verify(response);
assertEquals(Locale.FRENCH, mai.getInvocationContext().getLocale());
}
public void testSupportedLocaleRejectsUnsupportedRequestCookieLocale() throws Exception {
// given - request_cookie_locale=es but supportedLocale="en,fr"
prepare(I18nInterceptor.DEFAULT_COOKIE_PARAMETER, "es");
request.setPreferredLocales(Arrays.asList(Locale.ENGLISH));
interceptor.setSupportedLocale("en,fr");
HttpServletResponse response = EasyMock.createStrictMock(HttpServletResponse.class);
EasyMock.replay(response);
ac.put(StrutsStatics.HTTP_RESPONSE, response);
interceptor.setLocaleStorage(I18nInterceptor.Storage.COOKIE.name());
// when
interceptor.intercept(mai);
// then - unsupported request_cookie_locale ignored, falls back to Accept-Language match
EasyMock.verify(response);
assertEquals(Locale.ENGLISH, mai.getInvocationContext().getLocale());
}
public void testSupportedLocaleRevalidatesStoredCookieLocale() throws Exception {
// given - cookie has stored "de" but supportedLocale changed to "en,fr"
request.setCookies(new Cookie(I18nInterceptor.DEFAULT_COOKIE_ATTRIBUTE, "de"));
request.setPreferredLocales(Arrays.asList(Locale.ITALIAN));
interceptor.setSupportedLocale("en,fr");
HttpServletResponse response = EasyMock.createStrictMock(HttpServletResponse.class);
EasyMock.replay(response);
ac.put(StrutsStatics.HTTP_RESPONSE, response);
interceptor.setLocaleStorage(I18nInterceptor.Storage.COOKIE.name());
// when
interceptor.intercept(mai);
// then - stored "de" rejected and fallback locale from invocation context is used
EasyMock.verify(response);
assertEquals(Locale.US, mai.getInvocationContext().getLocale());
}
public void testRequestOnlyLocalePrecedenceWithSupportedLocale() throws Exception {
// given - request_only_locale should win over Accept-Language match
prepare(I18nInterceptor.DEFAULT_REQUEST_ONLY_PARAMETER, "fr");
request.setPreferredLocales(Arrays.asList(Locale.ENGLISH));
interceptor.setSupportedLocale("en,fr");
// when
interceptor.intercept(mai);
// then - request_only_locale applied and not persisted
assertNull(session.get(I18nInterceptor.DEFAULT_SESSION_ATTRIBUTE));
assertEquals(Locale.FRENCH, mai.getInvocationContext().getLocale());
}
public void testSupportedLocaleRejectsUnsupportedRequestOnlyLocale() throws Exception {
// given - request_only_locale=es but supportedLocale="en,fr"
prepare(I18nInterceptor.DEFAULT_REQUEST_ONLY_PARAMETER, "es");
request.setPreferredLocales(Arrays.asList(Locale.ENGLISH));
interceptor.setSupportedLocale("en,fr");
// when
interceptor.intercept(mai);
// then - es rejected, falls back to stored session locale / invocation context
assertNull(session.get(I18nInterceptor.DEFAULT_SESSION_ATTRIBUTE));
assertEquals(Locale.ENGLISH, mai.getInvocationContext().getLocale());
}
public void testAcceptLanguageBasedLocaleWithFallbackToDefault() throws Exception {
// given
request.setPreferredLocales(Arrays.asList(new Locale("da_DK"), new Locale("es")));
request.setPreferredLocales(Arrays.asList(Locale.forLanguageTag("da-DK"), Locale.forLanguageTag("es")));
interceptor.setLocaleStorage(null);
interceptor.setSupportedLocale("en,pl");
@@ -308,9 +440,9 @@ public class I18nInterceptorTest extends TestCase {
session = new HashMap<>();
ac = ActionContext.of()
.bind()
.withSession(session)
.withParameters(HttpParameters.create().build());
.bind()
.withSession(session)
.withParameters(HttpParameters.create().build());
request = new MockHttpServletRequest();
request.setSession(new MockHttpSession());
@@ -348,8 +480,8 @@ public class I18nInterceptorTest extends TestCase {
public boolean matches(Object argument) {
Cookie cookie = ((Cookie) argument);
return
(cookie.getName().equals(expected.getName()) &&
cookie.getValue().equals(expected.getValue()));
(cookie.getName().equals(expected.getName()) &&
cookie.getValue().equals(expected.getValue()));
}
public static Cookie eqCookie(Cookie ck) {
@@ -359,10 +491,10 @@ public class I18nInterceptorTest extends TestCase {
public void appendTo(StringBuffer buffer) {
buffer
.append("Received")
.append(expected.getName())
.append("/")
.append(expected.getValue());
.append("Received")
.append(expected.getName())
.append("/")
.append(expected.getValue());
}
}
@@ -217,6 +217,54 @@ public class HttpMethodInterceptorTest extends StrutsInternalTestCase {
assertEquals(HttpMethod.POST, action.getHttpMethod());
}
/**
* Simulates a wildcard action like {@code <action name="Wild-*" method="{1}">}
* resolving to method "onPostOnly" (annotated with @HttpPost).
* With the fix in DefaultActionProxy.resolveMethod(), config-resolved methods
* set isMethodSpecified()=true, so the interceptor checks method-level annotations.
* A GET request should be rejected because @HttpPost only allows POST.
*/
public void testWildcardResolvedMethodWithPostAnnotationRejectsGet() throws Exception {
// given
HttpMethodsTestAction action = new HttpMethodsTestAction();
prepareActionInvocation(action);
// Simulate wildcard resolution: Wild-onPostOnly -> method="onPostOnly"
actionProxy.setMethod("onPostOnly");
// After the fix, config-resolved methods have methodSpecified=true
actionProxy.setMethodSpecified(true);
invocation.setResultCode("onPostOnly");
prepareRequest("get");
// when
String resultName = interceptor.intercept(invocation);
// then - interceptor checks method-level @HttpPost and rejects GET
assertEquals("bad-request", resultName);
}
/**
* Counterpart: same wildcard scenario but with POST request should succeed.
*/
public void testWildcardResolvedMethodWithPostAnnotationAllowsPost() throws Exception {
// given
HttpMethodsTestAction action = new HttpMethodsTestAction();
prepareActionInvocation(action);
actionProxy.setMethod("onPostOnly");
actionProxy.setMethodSpecified(true);
invocation.setResultCode("onPostOnly");
prepareRequest("post");
// when
String resultName = interceptor.intercept(invocation);
// then - interceptor checks method-level @HttpPost and allows POST
assertEquals("onPostOnly", resultName);
}
private void prepareActionInvocation(Object action) {
interceptor = new HttpMethodInterceptor();
invocation = new MockActionInvocation();
@@ -42,7 +42,7 @@ import org.apache.struts2.ognl.accessor.RootAccessor;
import org.apache.struts2.util.ValueStack;
import org.apache.struts2.util.ValueStackFactory;
import org.apache.struts2.util.reflection.ReflectionContextState;
import ognl.OgnlContext;
import org.apache.struts2.ognl.StrutsContext;
import org.apache.struts2.action.NoParameters;
import org.apache.struts2.action.ParameterNameAware;
import org.apache.struts2.action.ParameterValueAware;
@@ -353,7 +353,7 @@ public class ParametersInterceptorTest extends XWorkTestCase {
//then
assertEquals("This is blah", ((SimpleAction) proxy.getAction()).getBlah());
Field field = ReflectionContextState.class.getField("DENY_METHOD_EXECUTION");
boolean allowStaticFieldAccess = ((OgnlContext) stack.getContext()).getMemberAccess().isAccessible((OgnlContext) stack.getContext(), ReflectionContextState.class, field, "");
boolean allowStaticFieldAccess = ((StrutsContext) stack.getContext()).getMemberAccess().isAccessible((StrutsContext) stack.getContext(), ReflectionContextState.class, field, "");
assertFalse(allowStaticFieldAccess);
}
@@ -30,7 +30,9 @@ import org.apache.struts2.ognl.DefaultOgnlBeanInfoCacheFactory;
import org.apache.struts2.ognl.DefaultOgnlExpressionCacheFactory;
import org.apache.struts2.ognl.OgnlUtil;
import org.apache.struts2.ognl.StrutsOgnlGuard;
import org.apache.struts2.ognl.StrutsProxyCacheFactory;
import org.apache.struts2.ognl.ThreadAllowlist;
import org.apache.struts2.util.StrutsProxyService;
import org.apache.struts2.security.AcceptedPatternsChecker.IsAccepted;
import org.apache.struts2.security.ExcludedPatternsChecker.IsExcluded;
import org.apache.struts2.security.NotExcludedAcceptedPatternsChecker;
@@ -71,6 +73,9 @@ public class StrutsParameterAnnotationTest {
new StrutsOgnlGuard());
parametersInterceptor.setOgnlUtil(ognlUtil);
var proxyService = new StrutsProxyService(new StrutsProxyCacheFactory<>("1000", "basic"));
parametersInterceptor.setProxyService(proxyService);
NotExcludedAcceptedPatternsChecker checker = mock(NotExcludedAcceptedPatternsChecker.class);
when(checker.isAccepted(anyString())).thenReturn(IsAccepted.yes(""));
when(checker.isExcluded(anyString())).thenReturn(IsExcluded.no(Set.of()));
@@ -23,7 +23,6 @@ import ognl.MethodFailedException;
import ognl.NoSuchPropertyException;
import ognl.NullHandler;
import ognl.Ognl;
import ognl.OgnlContext;
import ognl.OgnlException;
import ognl.OgnlRuntime;
import ognl.SimpleNode;
@@ -90,12 +89,12 @@ public class OgnlUtilTest extends XWorkTestCase {
public void testCanSetADependentObject() {
String dogName = "fido";
OgnlRuntime.setNullHandler(Owner.class, new NullHandler() {
public Object nullMethodResult(OgnlContext context, Object o, String s, Object[] objects) {
OgnlRuntime.setNullHandler(Owner.class, new NullHandler<StrutsContext>() {
public Object nullMethodResult(StrutsContext context, Object o, String s, Object[] objects) {
return null;
}
public Object nullPropertyValue(OgnlContext context, Object o, Object o1) {
public Object nullPropertyValue(StrutsContext context, Object o, Object o1) {
String methodName = o1.toString();
String getter = "set" + methodName.substring(0, 1).toUpperCase() + methodName.substring(1);
Method[] methods = o.getClass().getDeclaredMethods();
@@ -199,7 +198,7 @@ public class OgnlUtilTest extends XWorkTestCase {
public void testExpressionIsCachedIrrespectiveOfItsExecutionStatus() {
Foo foo = new Foo();
OgnlContext context = ognlUtil.createDefaultContext(foo);
StrutsContext context = ognlUtil.createDefaultContext(foo);
// Expression which executes with success
try {
@@ -223,7 +222,7 @@ public class OgnlUtilTest extends XWorkTestCase {
ognlUtil.setContainer(container); // Must be explicitly set as the generated OgnlUtil instance has no container
ognlUtil.setEnableExpressionCache("true");
Foo foo = new Foo();
OgnlContext context = ognlUtil.createDefaultContext(foo);
StrutsContext context = ognlUtil.createDefaultContext(foo);
// Expression which executes with success
try {
@@ -243,7 +242,7 @@ public class OgnlUtilTest extends XWorkTestCase {
public void testMethodExpressionIsCachedIrrespectiveOfItsExecutionStatus() {
Foo foo = new Foo();
OgnlContext context = ognlUtil.createDefaultContext(foo);
StrutsContext context = ognlUtil.createDefaultContext(foo);
// Method expression which executes with success
try {
@@ -846,7 +845,7 @@ public class OgnlUtilTest extends XWorkTestCase {
ChainingInterceptor foo = new ChainingInterceptor();
ChainingInterceptor foo2 = new ChainingInterceptor();
OgnlContext context = ognlUtil.createDefaultContext(null);
StrutsContext context = ognlUtil.createDefaultContext(null);
SimpleNode expression = (SimpleNode) Ognl.parseExpression("{'a','ruby','b','tom'}");
Ognl.getValue(expression, context, "aksdj");
@@ -903,7 +902,7 @@ public class OgnlUtilTest extends XWorkTestCase {
public void testBeanMapExpressions() throws OgnlException, NoSuchMethodException {
Foo foo = new Foo();
OgnlContext context = ognlUtil.createDefaultContext(foo);
StrutsContext context = ognlUtil.createDefaultContext(foo);
SecurityMemberAccess sma = (SecurityMemberAccess) context.getMemberAccess();
sma.useExcludedPackageNames("org.apache.struts2.ognl");
@@ -19,12 +19,13 @@
package org.apache.struts2.ognl;
import ognl.MemberAccess;
import ognl.OgnlContext;
import org.apache.commons.lang3.reflect.FieldUtils;
import org.apache.struts2.TestBean;
import org.apache.struts2.config.ConfigurationException;
import org.apache.struts2.test.TestBean2;
import org.apache.struts2.util.StrutsProxyService;
import org.apache.struts2.util.Foo;
import org.apache.struts2.util.ProxyService;
import org.hibernate.proxy.HibernateProxy;
import org.hibernate.proxy.LazyInitializer;
import org.junit.Before;
@@ -53,19 +54,21 @@ import static org.mockito.Mockito.when;
public class SecurityMemberAccessTest {
private OgnlContext context;
private StrutsContext context;
private FooBar target;
protected SecurityMemberAccess sma;
protected ProviderAllowlist mockedProviderAllowlist;
protected ThreadAllowlist mockedThreadAllowlist;
protected ProxyService proxyService;
@Before
public void setUp() {
context = ognl.Ognl.createDefaultContext(null);
target = new FooBar();
mockedProviderAllowlist = mock(ProviderAllowlist.class);
mockedThreadAllowlist = mock(ThreadAllowlist.class);
proxyService = new StrutsProxyService(new StrutsProxyCacheFactory<>("1000", "basic"));
assignNewSma(true);
context = new StrutsContext(sma);
}
protected void assignNewSma(boolean allowStaticFieldAccess) {
@@ -77,6 +80,7 @@ public class SecurityMemberAccessTest {
protected void assignNewSmaHelper() {
sma = new SecurityMemberAccess(mockedProviderAllowlist, mockedThreadAllowlist);
sma.setProxyService(proxyService);
}
private <T> T reflectField(String fieldName) throws IllegalAccessException {
@@ -37,7 +37,6 @@ import org.apache.struts2.util.ValueStack;
import org.apache.struts2.util.location.LocatableProperties;
import org.apache.struts2.util.reflection.ReflectionContextState;
import ognl.Ognl;
import ognl.OgnlContext;
import java.util.ArrayList;
import java.util.Collection;
@@ -57,7 +56,8 @@ public class SetPropertiesTest extends XWorkTestCase {
public void testOgnlUtilEmptyStringAsLong() {
Bar bar = new Bar();
OgnlContext context = Ognl.createDefaultContext(bar, new SecurityMemberAccess(null, null));
StrutsContext context = new StrutsContext(new SecurityMemberAccess(null, null));
context.withRoot(bar);
context.put(XWorkConverter.REPORT_CONVERSION_ERRORS, Boolean.TRUE);
bar.setId(null);
@@ -81,7 +81,7 @@ public class SetPropertiesTest extends XWorkTestCase {
ValueStack vs = ActionContext.getContext().getValueStack();
vs.getContext().put(XWorkConverter.REPORT_CONVERSION_ERRORS, Boolean.TRUE);
XWorkConverter c = (XWorkConverter) ((OgnlTypeConverterWrapper) Ognl.getTypeConverter((OgnlContext) vs.getContext())).getTarget();
XWorkConverter c = (XWorkConverter) ((OgnlTypeConverterWrapper) ((StrutsContext) vs.getContext()).getTypeConverter()).getTarget();
c.registerConverter(Cat.class.getName(), new FooBarConverter());
vs.push(foo);
@@ -97,7 +97,7 @@ public class SetPropertiesTest extends XWorkTestCase {
ValueStack vs = ActionContext.getContext().getValueStack();
vs.getContext().put(XWorkConverter.REPORT_CONVERSION_ERRORS, Boolean.TRUE);
XWorkConverter c = (XWorkConverter) ((OgnlTypeConverterWrapper) Ognl.getTypeConverter((OgnlContext) vs.getContext())).getTarget();
XWorkConverter c = (XWorkConverter) ((OgnlTypeConverterWrapper) ((StrutsContext) vs.getContext()).getTypeConverter()).getTarget();
c.registerConverter(Cat.class.getName(), new FooBarConverter());
vs.push(foo);

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