Compare commits

...

124 Commits

Author SHA1 Message Date
Lukasz Lenart c520a3c711 WW-5716 fix(tiles): bound the per-locale definition caches
The Tiles definition caches are keyed by the resolved Locale, which by
default derives from the request. Both CachingLocaleUrlDefinitionDAO's
locale2definitionMap and AbstractPatternDefinitionResolver's
localePatternPaths grew without limit and were never reduced for the
lifetime of the web application.

Bound locale2definitionMap with an insertion-order LinkedHashMap capped
at maxCachedLocales (default 1000, configurable via setMaxCachedLocales).
On eviction the DAO removes the same key from the pattern resolver via
the new PatternDefinitionResolver#removePatternPaths, keeping both maps
in lockstep (the resolver's keys are always a subset of the DAO's).
localePatternPaths becomes a ConcurrentHashMap since the DAO now removes
keys off the request thread. Eviction only re-incurs a load, never
changes rendering.

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

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015huuB72yvZygWEXKUYDAou
2026-09-04 10:54:29 +02:00
dependabot[bot] afcc85e172 build(deps): bump org.freemarker:freemarker from 2.3.34 to 2.3.35 (#1893)
Bumps org.freemarker:freemarker from 2.3.34 to 2.3.35.

---
updated-dependencies:
- dependency-name: org.freemarker:freemarker
  dependency-version: 2.3.35
  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-09-03 13:56:53 +02:00
dependabot[bot] 11353e3144 build(deps): bump org.easymock:easymock from 5.6.0 to 5.7.0 (#1896)
Bumps [org.easymock:easymock](https://github.com/easymock/easymock) from 5.6.0 to 5.7.0.
- [Release notes](https://github.com/easymock/easymock/releases)
- [Changelog](https://github.com/easymock/easymock/blob/master/ReleaseNotes.md)
- [Commits](https://github.com/easymock/easymock/compare/easymock-5.6.0...easymock-5.7.0)

---
updated-dependencies:
- dependency-name: org.easymock:easymock
  dependency-version: 5.7.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-09-03 13:56:40 +02:00
Lukasz Lenart 016c2ec9c3 WW-5711 fix(conversion): bound fraction digits when formatting BigDecimal (#1888)
StringConverter formatted BigDecimal, Double and Float with
maximumFractionDigits set to Integer.MAX_VALUE. That constant arrived
with WW-4871, which fixed round-trip precision loss for double and
float; both of those types are naturally bounded, the widest being
Double.MIN_VALUE at 325 fraction digits.

BigDecimal has no such bound. DecimalFormat honours
maximumFractionDigits literally and pads the fraction out to the
value's full scale, so the length of the formatted output followed the
scale of the value rather than its precision.

Bound the setting to 340. Every double and float value still formats in
full, as does every BigDecimal within that range; beyond it the value is
rounded to the bound. The existing round-trip assertions for
Double.MIN_VALUE (325 fraction digits) and for a BigDecimal slightly
wider than double (326) are untouched and still pass.

Backport of the same change on main, adjusted for the
com.opensymphony.xwork2 package layout of this line.

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


Claude-Session: https://claude.ai/code/session_01LwgeV4TN78ke2hHKTVWAUP

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-09-03 13:51:02 +02:00
Lukasz Lenart 52333e798f WW-5707 chore(core): deprecate legacy restful and restful2 action mappers (#1883)
The core restful (RestfulActionMapper) and restful2 (Restful2ActionMapper)
mappers predate the Struts REST plugin, which is the maintained way to build
REST-style applications. Mark both classes @Deprecated and point their javadoc
to the REST plugin; removal is tracked as WW-5708.

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


Claude-Session: https://claude.ai/code/session_016XMyQ1CRuYZkqygmD4aGHv

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-31 20:05:01 +02:00
Lukasz Lenart 107f2ed987 WW-5706 fix(core): align RestfulActionMapper action name handling with DefaultActionMapper (#1881)
RestfulActionMapper derived the action name straight from the request
URI, unlike DefaultActionMapper which validates it via cleanupActionName
against the allowedActionNames pattern. Apply the same check (and the
struts.allowed.action.names / struts.default.action.name settings) so
both mappers handle action names consistently.

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


Claude-Session: https://claude.ai/code/session_016XMyQ1CRuYZkqygmD4aGHv

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-31 20:04:31 +02:00
Lukasz Lenart 4f2ab44430 WW-5701 Compare the conversion marker by identity, not equals (6.x backport) (#1879)
* WW-5701 fix(conversion): compare the conversion marker by identity, not equals

Backport of the 7.4.0 fix (#1874) to the 6.x line.

NO_CONVERSION_POSSIBLE is an ordinary String constant, so comparing with
equals() also matched a genuinely converted element whose own text happens
to be "ognl.NoConversionPossible" - and silently dropped it from the
collection. Only the constant instance itself signals a failed conversion,
so compare by identity.

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

* WW-5701 test(conversion): cover the collection-source and single-value guard paths

Backport of the coverage tests added on main, where the Sonar quality gate
failed at 77.8% coverage of new code: the marker guard was only exercised on
the array-source path, leaving the false branch of the other two guards
uncovered.

Both added paths are reachable from a request - a Set-typed property fed from
a List, and a single-valued parameter assigned to a collection property. The
single-value holder is seeded before the assignment so that a setter which is
never called cannot make the test pass vacuously.

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

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-31 20:03:42 +02:00
Lukasz Lenart c2b97be299 WW-5700 fix(ognl): skip the store when a map or list element cannot be converted (#1878)
Backport of the 7.4.0 fix (#1873) to the 6.x line.

When XWorkConverter cannot convert a value it returns the marker string
NO_CONVERSION_POSSIBLE. The map and list property accessors stored that
marker straight into the target collection, so a Map<Long, Integer> could
end up holding the String "ognl.NoConversionPossible" under a String key,
and the next read of that collection failed with a ClassCastException far
from the cause. Skip the assignment instead and log at debug.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-31 20:02:43 +02:00
dependabot[bot] 4df864fd79 build(deps-dev): bump commons-validator:commons-validator from 1.10.1 to 1.11.0 (#1834)
Bumps [commons-validator:commons-validator](https://github.com/apache/commons-validator) from 1.10.1 to 1.11.0.
- [Changelog](https://github.com/apache/commons-validator/blob/master/RELEASE-NOTES.txt)
- [Commits](https://github.com/apache/commons-validator/compare/rel/commons-validator-1.10.1...rel/commons-validator-1.11.0)

---
updated-dependencies:
- dependency-name: commons-validator:commons-validator
  dependency-version: 1.11.0
  dependency-type: direct:development
  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-08-27 19:01:48 +02:00
dependabot[bot] be78ad873f build(deps-dev): bump org.apache.commons:commons-collections4 from 4.5.0 to 4.6.0 (#1842)
Bumps org.apache.commons:commons-collections4 from 4.5.0 to 4.6.0.

---
updated-dependencies:
- dependency-name: org.apache.commons:commons-collections4
  dependency-version: 4.6.0
  dependency-type: direct:development
  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-08-27 19:01:39 +02:00
dependabot[bot] 144a3b39d6 build(deps): bump jackson.version from 2.22.1 to 2.22.2 (#1869)
Bumps `jackson.version` from 2.22.1 to 2.22.2.

Updates `com.fasterxml.jackson.core:jackson-core` from 2.22.1 to 2.22.2
- [Commits](https://github.com/FasterXML/jackson-core/compare/jackson-core-2.22.1...jackson-core-2.22.2)

Updates `com.fasterxml.jackson.core:jackson-databind` from 2.22.1 to 2.22.2
- [Commits](https://github.com/FasterXML/jackson-databind/compare/jackson-databind-2.22.1...jackson-databind-2.22.2)

Updates `com.fasterxml.jackson.dataformat:jackson-dataformat-xml` from 2.22.1 to 2.22.2
- [Commits](https://github.com/FasterXML/jackson-dataformat-xml/compare/jackson-dataformat-xml-2.22.1...jackson-dataformat-xml-2.22.2)

---
updated-dependencies:
- dependency-name: com.fasterxml.jackson.core:jackson-core
  dependency-version: 2.22.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: com.fasterxml.jackson.core:jackson-databind
  dependency-version: 2.22.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: com.fasterxml.jackson.dataformat:jackson-dataformat-xml
  dependency-version: 2.22.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-08-27 19:01:15 +02:00
dependabot[bot] 128db2200c build(deps): bump org.apache.maven.wrapper:maven-wrapper from 3.2.0 to 3.3.4 (#1854)
Bumps [org.apache.maven.wrapper:maven-wrapper](https://github.com/apache/maven-wrapper) from 3.2.0 to 3.3.4.
- [Release notes](https://github.com/apache/maven-wrapper/releases)
- [Commits](https://github.com/apache/maven-wrapper/compare/maven-wrapper-3.2.0...maven-wrapper-3.3.4)

---
updated-dependencies:
- dependency-name: org.apache.maven.wrapper:maven-wrapper
  dependency-version: 3.3.4
  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-08-27 19:01:10 +02:00
Lukasz Lenart ca6312347f WW-5688 fix(rest): resolve id-bearing URIs into the root namespace when declared (#1863)
RestActionMapper mapped a URI carrying an id into the default namespace while
mapping the same action without an id into "/". Since the configuration only
fails over from "/" to "" and never the other way round, an action declared in
a package with namespace="/" resolved for index but 404'd for show, update and
destroy.

DefaultActionMapper already handles this: WW-2461 added a rootAvailable check
in June 2008, three months before WW-2820 reported the REST symptom, but the
fix was never ported to the mapper the REST plugin had forked earlier. Port it,
keeping the ordering that computes the action name while the namespace is still
empty, since the name is relative to it.

The promotion only fires when a package explicitly declares namespace="/" and
nothing more specific matched. Convention derives "" or "/sub" and never "/",
so applications that do not opt in are unaffected, and because a "/" lookup
already falls back to "", the set of resolvable actions is a superset of the
previous one.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-24 11:19:43 +02:00
dependabot[bot] c825b9fc61 build(deps): bump org.apache.maven:apache-maven from 3.9.6 to 3.9.16 (#1855)
Bumps org.apache.maven:apache-maven from 3.9.6 to 3.9.16.

---
updated-dependencies:
- dependency-name: org.apache.maven:apache-maven
  dependency-version: 3.9.16
  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-08-23 19:17:55 +02:00
dependabot[bot] dfb8481b04 build(deps): bump org.owasp:dependency-check-maven from 12.2.2 to 13.0.0 (#1841)
Bumps [org.owasp:dependency-check-maven](https://github.com/dependency-check/DependencyCheck) from 12.2.2 to 13.0.0.
- [Release notes](https://github.com/dependency-check/DependencyCheck/releases)
- [Changelog](https://github.com/dependency-check/DependencyCheck/blob/main/CHANGELOG.md)
- [Commits](https://github.com/dependency-check/DependencyCheck/compare/v12.2.2...v13.0.0)

---
updated-dependencies:
- dependency-name: org.owasp:dependency-check-maven
  dependency-version: 13.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-08-14 12:43:06 +02:00
Lukasz Lenart adaa2997f9 Prepare for a new development cycle of Struts 6.12.0 (#1827)
* [maven-release-plugin] prepare release STRUTS_6_11_0

* [maven-release-plugin] prepare for next development iteration
2026-08-14 12:40:12 +02:00
Lukasz Lenart 7ce27107e2 WW-5668 Make the localized-text provider caches size-bounded and align request-locale resolution (6.x) (#1823)
* WW-5668 Add remove(key) to the OgnlCache abstraction

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

* WW-5668 Bound the localized-text provider caches with configurable size

Converts bundlesMap, messageFormats and missingBundles to the existing
OgnlCache abstraction, configurable via struts.i18n.cacheType and
struts.i18n.cacheMaxSize (wtlfu / 10000 by default). The caches are kept
transient and rebuilt in readObject so the providers stay serializable,
and bundlesMap-related synchronization moves to a dedicated monitor since
the field is now reassignable.

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

* WW-5668 Add opt-in request-locale resolution consistency to Dispatcher

Adds struts.locale.validateRequestLocale (default false) so request-derived
locales can be restricted to the JVM's available-locale set, matching what
I18nInterceptor already applies to its own locale sources.

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

* WW-5668 Keep the localized-text providers deserializable across a version upgrade

Pins serialVersionUID to the value implicitly computed for the pre-6.11.0 class
shape instead of 1L, so a session serialized by a 6.10.0 node still loads on a
6.11.0 one during a rolling upgrade rather than failing with InvalidClassException.

Such a stream carries no value for the new cache settings, and field initialisers
do not run during deserialization, so readObject restores their defaults before
rebuilding the caches - without that guard it failed with a NullPointerException.

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

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 11:55:00 +02:00
Lukasz Lenart a1c8af5574 WW-5666 Apply input length limits consistently when reading request bodies (6.x) (#1822)
* WW-5666 fix(json): apply the input length limit while reading

* WW-5666 fix(core): bound the CSP report body read and make the limit configurable

CspReportAction read the submitted report body with a single readLine() and had
no limit of its own. Read it up to a limit instead, defaulting to 8192
characters and configurable through struts.csp.report.maxSize. A body above the
limit is discarded with a warning rather than processed.

The limit is injected when the action is built, before the interceptor stack
runs, because withServletRequest is invoked by the servletConfig interceptor
ahead of staticParams and params. Values that are not usable as a buffer size
are ignored with a warning.
2026-08-01 10:04:57 +02:00
dependabot[bot] 28657e5f29 build(deps): bump jackson.version from 2.22.0 to 2.22.1 (#1790)
Bumps `jackson.version` from 2.22.0 to 2.22.1.

Updates `com.fasterxml.jackson.core:jackson-core` from 2.22.0 to 2.22.1
- [Commits](https://github.com/FasterXML/jackson-core/compare/jackson-core-2.22.0...jackson-core-2.22.1)

Updates `com.fasterxml.jackson.core:jackson-databind` from 2.22.0 to 2.22.1
- [Commits](https://github.com/FasterXML/jackson/commits)

Updates `com.fasterxml.jackson.dataformat:jackson-dataformat-xml` from 2.22.0 to 2.22.1
- [Commits](https://github.com/FasterXML/jackson-dataformat-xml/compare/jackson-dataformat-xml-2.22.0...jackson-dataformat-xml-2.22.1)

---
updated-dependencies:
- dependency-name: com.fasterxml.jackson.core:jackson-core
  dependency-version: 2.22.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: com.fasterxml.jackson.core:jackson-databind
  dependency-version: 2.22.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: com.fasterxml.jackson.dataformat:jackson-dataformat-xml
  dependency-version: 2.22.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-07-17 20:21:13 +02:00
dependabot[bot] 1b1981e987 build(deps-dev): bump commons-logging:commons-logging (#1763)
Bumps [commons-logging:commons-logging](https://github.com/apache/commons-logging) from 1.3.6 to 1.4.0.
- [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.6...rel/commons-logging-1.4.0)

---
updated-dependencies:
- dependency-name: commons-logging:commons-logging
  dependency-version: 1.4.0
  dependency-type: direct:development
  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-07-12 11:38:07 +02:00
dependabot[bot] 1f16744114 build(deps): bump jackson.version from 2.21.4 to 2.22.0 (#1746)
Bumps `jackson.version` from 2.21.4 to 2.22.0.

Updates `com.fasterxml.jackson.core:jackson-core` from 2.21.4 to 2.22.0
- [Commits](https://github.com/FasterXML/jackson-core/compare/jackson-core-2.21.4...jackson-core-2.22.0)

Updates `com.fasterxml.jackson.core:jackson-databind` from 2.21.4 to 2.22.0
- [Commits](https://github.com/FasterXML/jackson/commits)

Updates `com.fasterxml.jackson.dataformat:jackson-dataformat-xml` from 2.21.4 to 2.22.0
- [Commits](https://github.com/FasterXML/jackson-dataformat-xml/compare/jackson-dataformat-xml-2.21.4...jackson-dataformat-xml-2.22.0)

---
updated-dependencies:
- dependency-name: com.fasterxml.jackson.core:jackson-core
  dependency-version: 2.22.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: com.fasterxml.jackson.core:jackson-databind
  dependency-version: 2.22.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: com.fasterxml.jackson.dataformat:jackson-dataformat-xml
  dependency-version: 2.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-07-12 11:35:01 +02:00
dependabot[bot] efda88b2bb build(deps): bump log4j2.version from 2.26.0 to 2.26.1 (#1772)
Bumps `log4j2.version` from 2.26.0 to 2.26.1.

Updates `org.apache.logging.log4j:log4j-api` from 2.26.0 to 2.26.1

Updates `org.apache.logging.log4j:log4j-core` from 2.26.0 to 2.26.1

Updates `org.apache.logging.log4j:log4j-jcl` from 2.26.0 to 2.26.1

Updates `org.apache.logging.log4j:log4j-slf4j-impl` from 2.26.0 to 2.26.1

Updates `org.apache.logging.log4j:log4j-web` from 2.26.0 to 2.26.1

---
updated-dependencies:
- dependency-name: org.apache.logging.log4j:log4j-api
  dependency-version: 2.26.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: org.apache.logging.log4j:log4j-core
  dependency-version: 2.26.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: org.apache.logging.log4j:log4j-jcl
  dependency-version: 2.26.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
- dependency-name: org.apache.logging.log4j:log4j-slf4j-impl
  dependency-version: 2.26.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: org.apache.logging.log4j:log4j-web
  dependency-version: 2.26.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-07-09 09:24:44 +02:00
dependabot[bot] 28db4ac276 build(deps-dev): bump org.apache.maven.plugins:maven-failsafe-plugin (#1764)
Bumps [org.apache.maven.plugins:maven-failsafe-plugin](https://github.com/apache/maven-surefire) from 3.5.5 to 3.5.6.
- [Release notes](https://github.com/apache/maven-surefire/releases)
- [Commits](https://github.com/apache/maven-surefire/compare/surefire-3.5.5...surefire-3.5.6)

---
updated-dependencies:
- dependency-name: org.apache.maven.plugins:maven-failsafe-plugin
  dependency-version: 3.5.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-07-01 12:56:48 +02:00
dependabot[bot] d46d581b07 build(deps): bump org.apache.maven.plugins:maven-dependency-plugin (#1754)
Bumps [org.apache.maven.plugins:maven-dependency-plugin](https://github.com/apache/maven-dependency-plugin) from 3.10.0 to 3.11.0.
- [Release notes](https://github.com/apache/maven-dependency-plugin/releases)
- [Commits](https://github.com/apache/maven-dependency-plugin/compare/maven-dependency-plugin-3.10.0...maven-dependency-plugin-3.11.0)

---
updated-dependencies:
- dependency-name: org.apache.maven.plugins:maven-dependency-plugin
  dependency-version: 3.11.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-06-26 07:17:57 +02:00
dependabot[bot] bc369df815 build(deps-dev): bump org.jacoco:jacoco-maven-plugin (#1750)
Bumps [org.jacoco:jacoco-maven-plugin](https://github.com/jacoco/jacoco) from 0.8.14 to 0.8.15.
- [Release notes](https://github.com/jacoco/jacoco/releases)
- [Commits](https://github.com/jacoco/jacoco/compare/v0.8.14...v0.8.15)

---
updated-dependencies:
- dependency-name: org.jacoco:jacoco-maven-plugin
  dependency-version: 0.8.15
  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-06-26 07:17:14 +02:00
dependabot[bot] d1b16a7657 build(deps): bump maven-surefire-plugin.version from 3.5.5 to 3.5.6 (#1748)
Bumps `maven-surefire-plugin.version` from 3.5.5 to 3.5.6.

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

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

---
updated-dependencies:
- dependency-name: org.apache.maven.surefire:surefire-junit47
  dependency-version: 3.5.6
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: org.apache.maven.plugins:maven-surefire-plugin
  dependency-version: 3.5.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-06-26 07:16:42 +02:00
Lukasz Lenart e2fa549b12 WW-5630 test(core): streamline ConfigParseUtilTest and convert to JUnit 4 (#1741)
Port of #1740 to 6.x: 12 overlapping cache tests collapsed to 5 focused
ones, JUnit 3 -> JUnit 4, dropped the ~80-class literal, reflection kept
only in the two size-bound tests. Also reorders the caffeine imports in
ConfigParseUtil to match alphabetical ordering. Production logic unchanged.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-14 18:36:28 +02:00
brianandle b455d6a8d4 WW-5630 - Performance Issue SecurityMemberAccess (#1721) (#1736)
* WW-5630 - Performance Issue SecurityMemberAccess
* Add size bound cache, 50, for Class lookup
* Add unit test

Code generated by Copilot

* WW-5630 - Add additional UT

* WW-5630 - Add UT for non-existent class

* WW-5630 - Review feedback changes
* Cache ClassLoader directly
* Use weakKeys and weakValues
* Comment on the ClassLookupException
* Additional Unit Tests

Assistance in coding using co-pilot

* WW-5630 - Additional review
* Limit outer, Classloader, to 25. Ensure memory bounding.
* Limit inner, Classes, to 50. Ensure memory bounding.
* Additional UTs

With co-pilot assitance

(cherry picked from commit 210dc86b88)
2026-06-14 13:27:11 +02:00
Lukasz Lenart f8d42eb023 Prepare for a new development cycle of Struts 6.11.0 (#1739)
* [maven-release-plugin] prepare release STRUTS_6_10_0

* [maven-release-plugin] prepare for next development iteration
2026-06-14 07:39:08 +00:00
dependabot[bot] f9f69482c7 build(deps): bump asm.version from 9.10 to 9.10.1 (#1728)
Bumps `asm.version` from 9.10 to 9.10.1.

Updates `org.ow2.asm:asm` from 9.10 to 9.10.1

Updates `org.ow2.asm:asm-commons` from 9.10 to 9.10.1

---
updated-dependencies:
- dependency-name: org.ow2.asm:asm
  dependency-version: 9.10.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: org.ow2.asm:asm-commons
  dependency-version: 9.10.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-06-13 08:29:17 +02:00
dependabot[bot] 0b14f39a2f build(deps): bump jackson.version from 2.21.3 to 2.21.4 (#1727)
Bumps `jackson.version` from 2.21.3 to 2.21.4.

Updates `com.fasterxml.jackson.core:jackson-core` from 2.21.3 to 2.21.4
- [Commits](https://github.com/FasterXML/jackson-core/compare/jackson-core-2.21.3...jackson-core-2.21.4)

Updates `com.fasterxml.jackson.core:jackson-databind` from 2.21.3 to 2.21.4
- [Commits](https://github.com/FasterXML/jackson/commits)

Updates `com.fasterxml.jackson.dataformat:jackson-dataformat-xml` from 2.21.3 to 2.21.4
- [Commits](https://github.com/FasterXML/jackson-dataformat-xml/compare/jackson-dataformat-xml-2.21.3...jackson-dataformat-xml-2.21.4)

---
updated-dependencies:
- dependency-name: com.fasterxml.jackson.core:jackson-core
  dependency-version: 2.21.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: com.fasterxml.jackson.core:jackson-databind
  dependency-version: 2.21.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: com.fasterxml.jackson.dataformat:jackson-dataformat-xml
  dependency-version: 2.21.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-06-10 13:21:25 +02:00
dependabot[bot] cf9b51669f build(deps): bump org.owasp:dependency-check-maven from 12.2.0 to 12.2.2 (#1726)
Bumps [org.owasp:dependency-check-maven](https://github.com/dependency-check/DependencyCheck) from 12.2.0 to 12.2.2.
- [Release notes](https://github.com/dependency-check/DependencyCheck/releases)
- [Changelog](https://github.com/dependency-check/DependencyCheck/blob/main/CHANGELOG.md)
- [Commits](https://github.com/dependency-check/DependencyCheck/compare/v12.2.0...v12.2.2)

---
updated-dependencies:
- dependency-name: org.owasp:dependency-check-maven
  dependency-version: 12.2.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-06-10 13:21:00 +02:00
dependabot[bot] bf0a695dcc build(deps-dev): bump org.apache.maven.plugins:maven-site-plugin (#1718)
Bumps [org.apache.maven.plugins:maven-site-plugin](https://github.com/apache/maven-site-plugin) from 3.21.0 to 3.22.0.
- [Release notes](https://github.com/apache/maven-site-plugin/releases)
- [Commits](https://github.com/apache/maven-site-plugin/compare/maven-site-plugin-3.21.0...maven-site-plugin-3.22.0)

---
updated-dependencies:
- dependency-name: org.apache.maven.plugins:maven-site-plugin
  dependency-version: 3.22.0
  dependency-type: direct:development
  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-05-28 07:19:41 +02:00
dependabot[bot] 8b8838a55a build(deps-dev): bump org.apache.maven.plugins:maven-assembly-plugin (#1717)
Bumps [org.apache.maven.plugins:maven-assembly-plugin](https://github.com/apache/maven-assembly-plugin) from 3.7.1 to 3.8.0.
- [Release notes](https://github.com/apache/maven-assembly-plugin/releases)
- [Commits](https://github.com/apache/maven-assembly-plugin/compare/maven-assembly-plugin-3.7.1...v3.8.0)

---
updated-dependencies:
- dependency-name: org.apache.maven.plugins:maven-assembly-plugin
  dependency-version: 3.8.0
  dependency-type: direct:development
  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-05-28 07:19:29 +02:00
dependabot[bot] bbb111db20 build(deps): bump jackson.version from 2.21.2 to 2.21.3 (#1716)
Bumps `jackson.version` from 2.21.2 to 2.21.3.

Updates `com.fasterxml.jackson.core:jackson-core` from 2.21.2 to 2.21.3
- [Commits](https://github.com/FasterXML/jackson-core/compare/jackson-core-2.21.2...jackson-core-2.21.3)

Updates `com.fasterxml.jackson.core:jackson-databind` from 2.21.2 to 2.21.3
- [Commits](https://github.com/FasterXML/jackson/commits)

Updates `com.fasterxml.jackson.dataformat:jackson-dataformat-xml` from 2.21.2 to 2.21.3
- [Commits](https://github.com/FasterXML/jackson-dataformat-xml/compare/jackson-dataformat-xml-2.21.2...jackson-dataformat-xml-2.21.3)

---
updated-dependencies:
- dependency-name: com.fasterxml.jackson.core:jackson-core
  dependency-version: 2.21.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: com.fasterxml.jackson.core:jackson-databind
  dependency-version: 2.21.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: com.fasterxml.jackson.dataformat:jackson-dataformat-xml
  dependency-version: 2.21.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-05-28 07:19:16 +02:00
dependabot[bot] ab08c59737 build(deps-dev): bump org.apache.maven.plugins:maven-enforcer-plugin (#1715)
Bumps [org.apache.maven.plugins:maven-enforcer-plugin](https://github.com/apache/maven-enforcer) from 3.6.2 to 3.6.3.
- [Release notes](https://github.com/apache/maven-enforcer/releases)
- [Commits](https://github.com/apache/maven-enforcer/compare/enforcer-3.6.2...enforcer-3.6.3)

---
updated-dependencies:
- dependency-name: org.apache.maven.plugins:maven-enforcer-plugin
  dependency-version: 3.6.3
  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-05-28 07:19:03 +02:00
Lukasz Lenart 0db282a51a pom(version): sets proper SNAPSHOT version (#1709) 2026-05-25 15:04:31 +00:00
Lukasz Lenart aaf286afa8 WW-5623 fix(core): HTML-encode form action in PostbackResult to prevent XSS (#1701)
PostbackResult.doExecute() embeds finalLocation into a <form action="">
attribute via raw string concatenation. A double quote in the location
breaks out of the attribute, enabling reflected XSS. The response
Content-Type is text/html.

Form field names and values elsewhere in the same class are properly
URL-encoded via URLEncoder.encode(); the action attribute was not
encoded at all.

Wrap finalLocation with StringEscapeUtils.escapeHtml4() before embedding
it in the form tag, consistent with the encoding approach used in
DefaultActionProxy, Property, and TextProviderHelper.

Adds three regression tests in PostbackResultTest:
- testFormActionHtmlEscaping: XSS payload with attribute breakout
- testFormActionEscapesAllHtmlSpecialChars: covers ", &, <, >
- testFormActionCleanLocationUnchanged: regression for clean URLs
2026-05-25 16:38:20 +02:00
Lukasz Lenart 84ef60eae8 WW-5535 fix(core): enforce class-level HTTP method annotations for wildcard-resolved unannotated methods (#1693)
The WW-5535 change to DefaultActionProxy.resolveMethod() (which made
wildcard-resolved methods report isMethodSpecified()=true) interacted
with HttpMethodInterceptor's if/else-if so that the class-level
annotation branch became unreachable when the resolved method carried
no method-level annotation:

    if (isMethodSpecified()) {
        if (method has annotation) return doIntercept(method);
        // unannotated method falls through silently
    } else if (class has annotation) {
        return doIntercept(class);  // never reached when methodSpecified=true
    }

Convert the else-if to a standalone if so the class-level check is
always evaluated as a fallback. Method-level annotations still take
precedence — they are checked first and return early.

Adds three tests:
- testWildcardResolvedUnannotatedMethodRespectsClassLevelAnnotation:
  GET on a wildcard-resolved unannotated method is rejected when the
  class is @AllowedHttpMethod(POST).
- testWildcardResolvedUnannotatedMethodAllowsPostWithClassLevelAnnotation:
  POST on the same configuration succeeds.
- testWildcardResolvedExecuteRejectsGetThroughRealProxy: end-to-end
  via a real DefaultActionProxy with <action name="Wild-*" method="{1}">,
  resolving to ActionSupport.execute().
2026-05-25 16:38:04 +02:00
dependabot[bot] fb35ed410c build(deps): bump log4j2.version from 2.25.4 to 2.26.0 (#1706)
Bumps `log4j2.version` from 2.25.4 to 2.26.0.

Updates `org.apache.logging.log4j:log4j-api` from 2.25.4 to 2.26.0

Updates `org.apache.logging.log4j:log4j-core` from 2.25.4 to 2.26.0

Updates `org.apache.logging.log4j:log4j-jcl` from 2.25.4 to 2.26.0

Updates `org.apache.logging.log4j:log4j-slf4j-impl` from 2.25.4 to 2.26.0

Updates `org.apache.logging.log4j:log4j-web` from 2.25.4 to 2.26.0

---
updated-dependencies:
- dependency-name: org.apache.logging.log4j:log4j-api
  dependency-version: 2.26.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: org.apache.logging.log4j:log4j-core
  dependency-version: 2.26.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: org.apache.logging.log4j:log4j-jcl
  dependency-version: 2.26.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
- dependency-name: org.apache.logging.log4j:log4j-slf4j-impl
  dependency-version: 2.26.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: org.apache.logging.log4j:log4j-web
  dependency-version: 2.26.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-05-20 09:58:16 +02:00
dependabot[bot] 9abd02d961 build(deps): bump asm.version from 9.9.1 to 9.10 (#1703)
Bumps `asm.version` from 9.9.1 to 9.10.

Updates `org.ow2.asm:asm` from 9.9.1 to 9.10

Updates `org.ow2.asm:asm-commons` from 9.9.1 to 9.10

---
updated-dependencies:
- dependency-name: org.ow2.asm:asm
  dependency-version: '9.10'
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: org.ow2.asm:asm-commons
  dependency-version: '9.10'
  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-05-20 09:56:17 +02:00
dependabot[bot] 59536d824a build(deps): bump slf4j.version from 2.0.17 to 2.0.18 (#1704)
Bumps `slf4j.version` from 2.0.17 to 2.0.18.

Updates `org.slf4j:slf4j-api` from 2.0.17 to 2.0.18

Updates `org.slf4j:slf4j-simple` from 2.0.17 to 2.0.18

---
updated-dependencies:
- dependency-name: org.slf4j:slf4j-api
  dependency-version: 2.0.18
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: org.slf4j:slf4j-simple
  dependency-version: 2.0.18
  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-05-20 09:26:54 +02:00
Lukasz Lenart ca1b22d9be Struts 6.9.0 (#1662)
* [maven-release-plugin] prepare release STRUTS_6_9_0

* [maven-release-plugin] prepare for next development iteration
2026-05-01 10:41:26 +02:00
Lukasz Lenart 59b5e47575 ci(scorecards): score cards analysis are only supported for default branch (#1661) 2026-04-10 07:13:17 +00:00
Lukasz Lenart 0e22570763 ci(struts6): adjust workflows to use the new branch names (#1658) 2026-04-10 08:23:04 +02:00
Lukasz Lenart 94e3ffd1e7 WW-5622 perf(core): cache Hibernate class presence to avoid repeated NoClassDefFoundError (#1650)
Detect Hibernate availability once at class-load time via Class.forName()
and short-circuit all Hibernate-related methods immediately when absent.
This eliminates repeated NoClassDefFoundError exceptions that cause
significant performance degradation in applications without Hibernate
on the classpath.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-05 18:30:57 +02:00
Lukasz Lenart b936fcbf8b WW-5621 Harden XML parsers against Entity Expansion (Billion Laughs) attacks (#1643)
Backport of apache/struts#1642 from Struts 7 to Struts 6.

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.

- Enable FEATURE_SECURE_PROCESSING in DomHelper SAX parser
- Enable FEATURE_SECURE_PROCESSING in DigesterDefinitionsReader
- Remove unused parseStringAsXML feature from StringAdapter to eliminate
  a theoretical XML Entity Expansion vector
- Deprecate setParseStringAsXML() and getParseStringAsXML() for removal
- Add Billion Laughs protection tests for DomHelper and DigesterDefinitionsReader

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-04 11:20:47 +02:00
dependabot[bot] 39c1e29958 build(deps): bump log4j2.version from 2.25.3 to 2.25.4 (#1647)
Bumps `log4j2.version` from 2.25.3 to 2.25.4.

Updates `org.apache.logging.log4j:log4j-api` from 2.25.3 to 2.25.4

Updates `org.apache.logging.log4j:log4j-core` from 2.25.3 to 2.25.4

Updates `org.apache.logging.log4j:log4j-jcl` from 2.25.3 to 2.25.4

Updates `org.apache.logging.log4j:log4j-slf4j-impl` from 2.25.3 to 2.25.4

Updates `org.apache.logging.log4j:log4j-web` from 2.25.3 to 2.25.4

---
updated-dependencies:
- dependency-name: org.apache.logging.log4j:log4j-api
  dependency-version: 2.25.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: org.apache.logging.log4j:log4j-core
  dependency-version: 2.25.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: org.apache.logging.log4j:log4j-jcl
  dependency-version: 2.25.4
  dependency-type: direct:development
  update-type: version-update:semver-patch
- dependency-name: org.apache.logging.log4j:log4j-slf4j-impl
  dependency-version: 2.25.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: org.apache.logging.log4j:log4j-web
  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:08 +02:00
Lukasz Lenart 6c0189560b ci: fix nightly publishing on release/struts-6-8-x (#1644)
* ci: update Jenkinsfile branch guards from struts-6-7-x to struts-6-8-x

The Build Source & JavaDoc, Deploy Snapshot, and Upload nightlies stages
still referenced release/struts-6-7-x in their when conditions, causing
them to be skipped on every release/struts-6-8-x build. This prevented
6.9.0-SNAPSHOT artifacts from being published to nightlies.

Made-with: Cursor

* chore: add .metals, .bloop, and .vscode to .gitignore

Made-with: Cursor
2026-03-30 12:37:45 +02:00
Lukasz Lenart 0a8b111e36 WW-5537 fix(core): resolve classloader/memory leaks during Tomcat hot deployment (#1631)
* WW-5537 fix(core): resolve classloader/memory leaks during Tomcat hot deployment

Introduce InternalDestroyable interface with container-based discovery to
clean up static caches, daemon threads, and shared references that pin the
webapp classloader after undeploy. This prevents OutOfMemoryError (Metaspace)
on repeated hot deployments.

Changes:
- Add InternalDestroyable/ContextAwareDestroyable interfaces for cleanup hooks
- Clear OGNL, Component, ScopeInterceptor, DefaultFileManager static caches
- Stop FinalizableReferenceQueue daemon thread and null its classloader
- Clear FreeMarker template/introspection caches from ServletContext
- Replace ContainerHolder ThreadLocal with volatile to prevent thread-pool leaks
- Clear static dispatcherListeners list on Dispatcher cleanup
- Add JSONCacheDestroyable for json plugin cache cleanup
- Register all destroyables via struts-beans.xml / struts-plugin.xml

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

* WW-5537 chore(showcase): add log4j-web for proper Log4j2 lifecycle in Servlet container

Without log4j-web, Log4j2 SoftReferences delay classloader GC after undeploy.
The log4j-web module provides Log4jServletContextListener which ensures proper
Log4j2 shutdown during ServletContext destruction.

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

* WW-5537 fix(core): use ThreadLocal with generation counter in ContainerHolder

Replace the volatile shared reference with a ThreadLocal backed by a volatile
generation counter. Per-request clear() only affects the current thread (safe
for concurrent requests and tests). On undeploy, invalidateAll() advances the
generation counter so idle pool threads detect staleness on next access and
self-clear, preventing classloader leaks without breaking test isolation.

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:44 +02:00
dependabot[bot] 696ee73b72 build(deps): bump org.apache.maven.doxia:doxia-module-markdown (#1638)
Bumps org.apache.maven.doxia:doxia-module-markdown from 2.0.0 to 2.1.0.

---
updated-dependencies:
- dependency-name: org.apache.maven.doxia:doxia-module-markdown
  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:08:32 +01:00
dependabot[bot] be3db87b7f build(deps): bump org.apache.maven.doxia:doxia-core from 2.0.0 to 2.1.0 (#1636)
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:07:54 +01:00
dependabot[bot] bd93c1bc19 build(deps): bump jackson.version from 2.21.1 to 2.21.2 (#1635)
Bumps `jackson.version` from 2.21.1 to 2.21.2.

Updates `com.fasterxml.jackson.core:jackson-core` from 2.21.1 to 2.21.2
- [Commits](https://github.com/FasterXML/jackson-core/compare/jackson-core-2.21.1...jackson-core-2.21.2)

Updates `com.fasterxml.jackson.core:jackson-databind` from 2.21.1 to 2.21.2
- [Commits](https://github.com/FasterXML/jackson/commits)

Updates `com.fasterxml.jackson.dataformat:jackson-dataformat-xml` from 2.21.1 to 2.21.2
- [Commits](https://github.com/FasterXML/jackson-dataformat-xml/compare/jackson-dataformat-xml-2.21.1...jackson-dataformat-xml-2.21.2)

---
updated-dependencies:
- dependency-name: com.fasterxml.jackson.core:jackson-core
  dependency-version: 2.21.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: com.fasterxml.jackson.core:jackson-databind
  dependency-version: 2.21.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: com.fasterxml.jackson.dataformat:jackson-dataformat-xml
  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:07:16 +01:00
Lukasz Lenart f644ea54e2 WW-5618 feat(json): add configurable limits to JSON plugin for DoS prevention (#1626)
Add configurable limits to the JSON plugin to prevent denial-of-service
attacks via malicious JSON payloads. Limits are enforced directly in the
existing JSONReader class without breaking backward compatibility (no
interface extraction or class renames).

New configurable constants (struts-plugin.xml defaults):
- struts.json.maxElements (10000) - per-container element count
- struts.json.maxDepth (64) - maximum nesting depth
- struts.json.maxLength (2097152) - maximum input length in chars
- struts.json.maxStringLength (262144) - maximum string value length
- struts.json.maxKeyLength (512) - maximum object key length

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-21 12:11:22 +01:00
Lukasz Lenart 0c47627910 WW-2963 fix(core): resolve default-action-ref via wildcard matching (#1623)
When a default-action-ref points to an action name that only exists as a
wildcard pattern (e.g. "movie-input" matching "movie-*"), the framework
now falls back to wildcard matching after the exact lookup fails.

Port of PR #1614 from Struts 7 to Struts 6.x.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-16 07:25:30 +01:00
Lukasz Lenart bfebc3e4a1 WW-4428 feat(json): add java.time serialization and deserialization support (#1616)
- 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
- Add Calendar deserialization support (was serialize-only)
- Add comprehensive tests including custom formats, null handling,
  malformed input, and round-trip serialization/deserialization

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-12 08:43:08 +01:00
dependabot[bot] 5a90581033 build(deps-dev): bump commons-logging:commons-logging (#1621)
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:50 +01:00
Lukasz Lenart edd604a9c5 Sets a proper SNAPSHOT version before next release (#1615) 2026-03-09 10:24:37 +01:00
Lukasz Lenart a0d4f21ad4 Simplifies branch namming patter (#1605) 2026-03-09 09:57:57 +01:00
Lukasz Lenart df97ee23dc fix(i18n): WW-5549 validate locale parameters against supportedLocale (#1602)
When supportedLocale is configured on I18nInterceptor, request_locale
and request_cookie_locale parameters were ignored because
AcceptLanguageLocaleHandler.find() matched the Accept-Language header
before session/cookie handlers checked their explicit locale parameters.
Additionally, stored locales (session/cookie) were never validated
against supportedLocale.

Changes:
- Add isLocaleSupported() helper to validate locales against config
- RequestLocaleHandler.find() now validates against supportedLocale
- AcceptLanguageLocaleHandler.find() checks request_only_locale first,
  then falls back to Accept-Language matching
- SessionLocaleHandler.find() checks request_locale before super.find()
- CookieLocaleHandler.find() checks request_cookie_locale before
  super.find()
- SessionLocaleHandler.read() discards stale session locales
- CookieLocaleHandler.read() discards stale cookie locales

Port of PR #1594 (bug fix only, no refactoring)

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

Co-authored-by: Claude <noreply@anthropic.com>
2026-03-06 07:50:38 +01:00
Lukasz Lenart 66ea9eaf24 fix(core): WW-5535 enforce HTTP method annotations for wildcard actions (#1593)
DefaultActionProxy.resolveMethod() incorrectly set methodSpecified=false
for config-resolved methods (including wildcard-substituted ones), causing
HttpMethodInterceptor to skip method-level @HttpPost/@HttpGet annotation
checks. Move methodSpecified=false inside the inner if block so it only
applies when truly defaulting to "execute".

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

Co-authored-by: Claude <noreply@anthropic.com>
2026-02-27 13:26:06 +01:00
dependabot[bot] 66e02ba621 build(deps-dev): bump org.apache.maven.plugins:maven-failsafe-plugin (#1601)
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 09:00:20 +01:00
dependabot[bot] 0e16915b4f build(deps): bump maven-surefire-plugin.version from 3.5.4 to 3.5.5 (#1600)
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 09:00:10 +01:00
dependabot[bot] 7b99d9eacb build(deps): bump jackson.version from 2.21.0 to 2.21.1 (#1599)
Bumps `jackson.version` from 2.21.0 to 2.21.1.

Updates `com.fasterxml.jackson.core:jackson-core` from 2.21.0 to 2.21.1
- [Commits](https://github.com/FasterXML/jackson-core/compare/jackson-core-2.21.0...jackson-core-2.21.1)

Updates `com.fasterxml.jackson.core:jackson-databind` from 2.21.0 to 2.21.1
- [Commits](https://github.com/FasterXML/jackson/commits)

Updates `com.fasterxml.jackson.dataformat:jackson-dataformat-xml` from 2.21.0 to 2.21.1
- [Commits](https://github.com/FasterXML/jackson-dataformat-xml/compare/jackson-dataformat-xml-2.21.0...jackson-dataformat-xml-2.21.1)

---
updated-dependencies:
- dependency-name: com.fasterxml.jackson.core:jackson-core
  dependency-version: 2.21.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: com.fasterxml.jackson.core:jackson-databind
  dependency-version: 2.21.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: com.fasterxml.jackson.dataformat:jackson-dataformat-xml
  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 09:00:00 +01:00
brianandle d4a549672f WW-5616 - JakartaStreamMultiPartRequest warns on file delete if the file doesnt exist (#1591)
* Pull aspects into alignment with main/7.x+ AbstractMultiPartRequest.java
* Update JakartaMultiPartRequest and JakartaStreamMultiPartRequest to use isFile()
* Update cleanup text to mirror main/7.x+
2026-02-22 08:40:15 +01:00
Lukasz Lenart 4b2915682d fix(convention): WW-4421 detect duplicate @Action names when execute() is annotated (#1590)
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.

Backport of apache/struts#1579 from Struts 7.x to 6.x.

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

Co-authored-by: Claude <noreply@anthropic.com>
2026-02-21 18:18:27 +01:00
Lukasz Lenart 18d6e77bf2 WW-5514: Make ProxyUtil cache configurable via struts constants (#1573)
* fix(ognl): make ProxyUtil cache configurable via struts constants

Makes the ProxyUtil cache type configurable through Struts constants,
allowing applications to use BASIC cache type (default) without
requiring Caffeine as a mandatory dependency.

New configuration properties:
- struts.proxy.cacheType: basic (default), lru, or wtlfu
- struts.proxy.cacheMaxSize: 10000 (default)

Fixes WW-5514

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

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

* refactor(ognl): use LazyRef for proxy caches and reset on factory change

Extract lazy initialization into reusable LazyRef<T> utility with
double-checked locking and reset support. ProxyUtil.setProxyCacheFactory()
now resets both caches so they are recreated with the new factory,
fixing the bug where caches were never refreshed after factory changes.
Default proxy cache type changed from 'basic' to 'wtlfu' for consistency
with expression and beanInfo caches. Fix @since version to 6.9.0.

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

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-02-21 18:17:39 +01:00
dependabot[bot] 2bd1b60802 build(deps): bump org.apache.maven.plugins:maven-dependency-plugin (#1576)
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:58 +02:00
Kusal Kithul-Godage 43b83731b7 Merge pull request #1565 from apache/WW-5610-extend-forwards-compat
WW-5610 Extend Struts 7 forwards compat to more interceptors
2026-02-12 21:59:28 +11:00
Lukasz Lenart 122dec4d73 feat(conversion): WW-4291 allow Spring bean names for type converters (#1564)
Implement two-phase processing for conversion properties to enable
Spring bean name resolution in struts-conversion.properties files.

The issue was a timing problem: type converters were processed during
bootstrap phase before SpringObjectFactory was available. Now:
- Early phase: process struts-default-conversion.properties (class names)
- Late phase: process user properties when SpringObjectFactory is ready

Changes:
- Add UserConversionPropertiesProvider interface for late initialization
- Add UserConversionPropertiesProcessor to trigger late phase processing
- Split StrutsConversionPropertiesProcessor.init() into early/late phases
- Register new beans in DefaultConfiguration and struts-beans.xml
- Add alias in StrutsBeanSelectionProvider for dependency injection
- Improve JavaDocs for BeanSelectionProvider classes

Closes WW-4291

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

Co-authored-by: Claude <noreply@anthropic.com>
2026-02-06 07:44:06 +01:00
dependabot[bot] 2907291ef7 build(deps): bump org.owasp:dependency-check-maven from 12.1.9 to 12.2.0 (#1527)
Bumps [org.owasp:dependency-check-maven](https://github.com/dependency-check/DependencyCheck) from 12.1.9 to 12.2.0.
- [Release notes](https://github.com/dependency-check/DependencyCheck/releases)
- [Changelog](https://github.com/dependency-check/DependencyCheck/blob/main/CHANGELOG.md)
- [Commits](https://github.com/dependency-check/DependencyCheck/compare/v12.1.9...v12.2.0)

---
updated-dependencies:
- dependency-name: org.owasp:dependency-check-maven
  dependency-version: 12.2.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-02 19:56:44 +01:00
dependabot[bot] 7065af8304 build(deps): bump jackson.version from 2.20.1 to 2.21.0 (#1550)
Bumps `jackson.version` from 2.20.1 to 2.21.0.

Updates `com.fasterxml.jackson.core:jackson-core` from 2.20.1 to 2.21.0
- [Commits](https://github.com/FasterXML/jackson-core/compare/jackson-core-2.20.1...jackson-core-2.21.0)

Updates `com.fasterxml.jackson.core:jackson-databind` from 2.20.1 to 2.21.0
- [Commits](https://github.com/FasterXML/jackson/commits)

Updates `com.fasterxml.jackson.dataformat:jackson-dataformat-xml` from 2.20.1 to 2.21.0
- [Commits](https://github.com/FasterXML/jackson-dataformat-xml/compare/jackson-dataformat-xml-2.20.1...jackson-dataformat-xml-2.21.0)

---
updated-dependencies:
- dependency-name: com.fasterxml.jackson.core:jackson-core
  dependency-version: 2.21.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: com.fasterxml.jackson.core:jackson-databind
  dependency-version: 2.21.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: com.fasterxml.jackson.dataformat:jackson-dataformat-xml
  dependency-version: 2.21.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-02 19:50:51 +01:00
dependabot[bot] 08fe3a2267 build(deps): bump org.apache.commons:commons-text from 1.12.0 to 1.15.0 (#1549)
Bumps [org.apache.commons:commons-text](https://github.com/apache/commons-text) from 1.12.0 to 1.15.0.
- [Changelog](https://github.com/apache/commons-text/blob/master/RELEASE-NOTES.txt)
- [Commits](https://github.com/apache/commons-text/compare/rel/commons-text-1.12.0...rel/commons-text-1.15.0)

---
updated-dependencies:
- dependency-name: org.apache.commons:commons-text
  dependency-version: 1.15.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-02 19:43:30 +01:00
dependabot[bot] ff496c8850 build(deps): bump org.assertj:assertj-core from 3.27.6 to 3.27.7 (#1560)
Bumps [org.assertj:assertj-core](https://github.com/assertj/assertj) from 3.27.6 to 3.27.7.
- [Release notes](https://github.com/assertj/assertj/releases)
- [Commits](https://github.com/assertj/assertj/compare/assertj-build-3.27.6...assertj-build-3.27.7)

---
updated-dependencies:
- dependency-name: org.assertj:assertj-core
  dependency-version: 3.27.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-02-02 18:51:32 +01:00
Kusal Kithul-Godage 87d8feaa8f WW-5610 Extend Struts 7 forwards compat to more interceptors 2026-02-02 18:27:01 +11:00
Lukasz Lenart a5b736b488 chore(conf): skips scans if PR created by Dependabot (#1554) 2026-01-26 16:21:39 +01:00
dependabot[bot] 55ed8629fe build(deps): bump org.apache.velocity:velocity-engine-core (#1546)
Bumps org.apache.velocity:velocity-engine-core from 2.3 to 2.4.1.

---
updated-dependencies:
- dependency-name: org.apache.velocity:velocity-engine-core
  dependency-version: 2.4.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-01-26 12:00:20 +01:00
dependabot[bot] a3820104d1 build(deps): bump commons-beanutils:commons-beanutils (#1544)
Bumps commons-beanutils:commons-beanutils from 1.9.4 to 1.11.0.

---
updated-dependencies:
- dependency-name: commons-beanutils:commons-beanutils
  dependency-version: 1.11.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-01-26 11:57:28 +01:00
dependabot[bot] 984383023b build(deps-dev): bump org.apache.maven.plugins:maven-wrapper-plugin (#1542)
Bumps [org.apache.maven.plugins:maven-wrapper-plugin](https://github.com/apache/maven-wrapper) from 3.3.3 to 3.3.4.
- [Release notes](https://github.com/apache/maven-wrapper/releases)
- [Commits](https://github.com/apache/maven-wrapper/compare/maven-wrapper-3.3.3...maven-wrapper-3.3.4)

---
updated-dependencies:
- dependency-name: org.apache.maven.plugins:maven-wrapper-plugin
  dependency-version: 3.3.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-01-26 10:24:25 +01:00
dependabot[bot] ba6572eab1 build(deps-dev): bump org.codehaus.mojo:versions-maven-plugin (#1535)
Bumps [org.codehaus.mojo:versions-maven-plugin](https://github.com/mojohaus/versions) from 2.20.1 to 2.21.0.
- [Release notes](https://github.com/mojohaus/versions/releases)
- [Changelog](https://github.com/mojohaus/versions/blob/master/ReleaseNotes.md)
- [Commits](https://github.com/mojohaus/versions/compare/2.20.1...2.21.0)

---
updated-dependencies:
- dependency-name: org.codehaus.mojo:versions-maven-plugin
  dependency-version: 2.21.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-01-20 13:11:55 +01:00
dependabot[bot] 921f07f091 build(deps): bump org.apache.maven.plugins:maven-source-plugin (#1532)
Bumps [org.apache.maven.plugins:maven-source-plugin](https://github.com/apache/maven-source-plugin) from 3.3.1 to 3.4.0.
- [Release notes](https://github.com/apache/maven-source-plugin/releases)
- [Commits](https://github.com/apache/maven-source-plugin/compare/maven-source-plugin-3.3.1...maven-source-plugin-3.4.0)

---
updated-dependencies:
- dependency-name: org.apache.maven.plugins:maven-source-plugin
  dependency-version: 3.4.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-01-20 10:39:13 +01:00
dependabot[bot] 6e15fbde32 build(deps-dev): bump commons-validator:commons-validator (#1523)
Bumps [commons-validator:commons-validator](https://github.com/apache/commons-validator) from 1.10.0 to 1.10.1.
- [Changelog](https://github.com/apache/commons-validator/blob/master/RELEASE-NOTES.txt)
- [Commits](https://github.com/apache/commons-validator/compare/rel/commons-validator-1.10.0...rel/commons-validator-1.10.1)

---
updated-dependencies:
- dependency-name: commons-validator:commons-validator
  dependency-version: 1.10.1
  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-01-14 19:19:51 +01:00
Lukasz Lenart c60670b22b WW-5602 fix StreamResult contentCharSet handling (#1511)
Evaluates contentCharSet expression before emptiness check to prevent
malformed content-type headers when expression evaluates to null.

- Parse contentCharSet expression first, then check if result is empty
- Use StringUtils.isNotEmpty() for proper null/empty validation
- Use setCharacterEncoding() instead of appending to content-type string
- Add test for null-evaluating charset expressions

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

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-14 19:18:27 +01:00
dependabot[bot] 6d20e33a37 build(deps): bump io.github.x-stream:mxparser from 1.2.1 to 1.2.3 (#1517)
Bumps [io.github.x-stream:mxparser](https://github.com/x-stream/mxparser) from 1.2.1 to 1.2.3.
- [Changelog](https://github.com/x-stream/mxparser/blob/master/changes.xml)
- [Commits](https://github.com/x-stream/mxparser/compare/v-1.2.1...v-1.2.3)

---
updated-dependencies:
- dependency-name: io.github.x-stream:mxparser
  dependency-version: 1.2.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-01-12 19:26:49 +01:00
dependabot[bot] 43330ec5c5 build(deps): bump org.easymock:easymock from 5.4.0 to 5.6.0 (#1520)
Bumps [org.easymock:easymock](https://github.com/easymock/easymock) from 5.4.0 to 5.6.0.
- [Release notes](https://github.com/easymock/easymock/releases)
- [Changelog](https://github.com/easymock/easymock/blob/master/ReleaseNotes.md)
- [Commits](https://github.com/easymock/easymock/compare/easymock-5.4.0...easymock-5.6.0)

---
updated-dependencies:
- dependency-name: org.easymock:easymock
  dependency-version: 5.6.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-01-12 19:26:23 +01:00
dependabot[bot] 752c00f1ae build(deps): bump org.apache.maven.plugins:maven-war-plugin (#1519)
Bumps [org.apache.maven.plugins:maven-war-plugin](https://github.com/apache/maven-war-plugin) from 3.4.0 to 3.5.1.
- [Release notes](https://github.com/apache/maven-war-plugin/releases)
- [Commits](https://github.com/apache/maven-war-plugin/compare/maven-war-plugin-3.4.0...maven-war-plugin-3.5.1)

---
updated-dependencies:
- dependency-name: org.apache.maven.plugins:maven-war-plugin
  dependency-version: 3.5.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-01-12 19:25:59 +01:00
Lukasz Lenart f3408b756d Merge pull request #1516 from apache/dependabot/maven/release/struts-6-8-x/commons-logging-commons-logging-1.3.5
build(deps-dev): bump commons-logging:commons-logging from 1.3.4 to 1.3.5
2026-01-12 19:25:42 +01:00
dependabot[bot] 1a17211d05 build(deps-dev): bump commons-logging:commons-logging
Bumps commons-logging:commons-logging from 1.3.4 to 1.3.5.

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-01-12 17:52:27 +00:00
dependabot[bot] d955721e17 build(deps): bump org.codehaus.mojo:exec-maven-plugin (#1509)
Bumps [org.codehaus.mojo:exec-maven-plugin](https://github.com/mojohaus/exec-maven-plugin) from 3.6.2 to 3.6.3.
- [Release notes](https://github.com/mojohaus/exec-maven-plugin/releases)
- [Commits](https://github.com/mojohaus/exec-maven-plugin/compare/3.6.2...3.6.3)

---
updated-dependencies:
- dependency-name: org.codehaus.mojo:exec-maven-plugin
  dependency-version: 3.6.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-01-12 18:41:04 +01:00
dependabot[bot] 7f96a4f16e build(deps): bump org.codehaus.mojo:versions-maven-plugin (#1501)
Bumps [org.codehaus.mojo:versions-maven-plugin](https://github.com/mojohaus/versions) from 2.17.1 to 2.20.1.
- [Release notes](https://github.com/mojohaus/versions/releases)
- [Changelog](https://github.com/mojohaus/versions/blob/master/ReleaseNotes.md)
- [Commits](https://github.com/mojohaus/versions/compare/2.17.1...2.20.1)

---
updated-dependencies:
- dependency-name: org.codehaus.mojo:versions-maven-plugin
  dependency-version: 2.20.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>
2025-12-29 13:36:20 +01:00
dependabot[bot] 4982e61f09 build(deps): bump org.owasp:dependency-check-maven from 10.0.4 to 12.1.9 (#1500)
Bumps [org.owasp:dependency-check-maven](https://github.com/dependency-check/DependencyCheck) from 10.0.4 to 12.1.9.
- [Release notes](https://github.com/dependency-check/DependencyCheck/releases)
- [Changelog](https://github.com/dependency-check/DependencyCheck/blob/main/CHANGELOG.md)
- [Commits](https://github.com/dependency-check/DependencyCheck/compare/v10.0.4...v12.1.9)

---
updated-dependencies:
- dependency-name: org.owasp:dependency-check-maven
  dependency-version: 12.1.9
  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>
2025-12-29 13:36:08 +01:00
dependabot[bot] 71048d41a5 build(deps): bump org.apache.maven.plugins:maven-release-plugin (#1499)
Bumps [org.apache.maven.plugins:maven-release-plugin](https://github.com/apache/maven-release) from 3.3.0 to 3.3.1.
- [Release notes](https://github.com/apache/maven-release/releases)
- [Commits](https://github.com/apache/maven-release/compare/maven-release-3.3.0...maven-release-3.3.1)

---
updated-dependencies:
- dependency-name: org.apache.maven.plugins:maven-release-plugin
  dependency-version: 3.3.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>
2025-12-29 13:35:54 +01:00
dependabot[bot] e8640b21f1 build(deps): bump asm.version from 9.9 to 9.9.1 (#1495)
Bumps `asm.version` from 9.9 to 9.9.1.

Updates `org.ow2.asm:asm` from 9.9 to 9.9.1

Updates `org.ow2.asm:asm-commons` from 9.9 to 9.9.1

---
updated-dependencies:
- dependency-name: org.ow2.asm:asm
  dependency-version: 9.9.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: org.ow2.asm:asm-commons
  dependency-version: 9.9.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>
2025-12-29 11:23:43 +01:00
dependabot[bot] daa9e9c9e1 build(deps): bump org.apache.maven.plugins:maven-site-plugin (#1494)
Bumps [org.apache.maven.plugins:maven-site-plugin](https://github.com/apache/maven-site-plugin) from 3.20.0 to 3.21.0.
- [Release notes](https://github.com/apache/maven-site-plugin/releases)
- [Commits](https://github.com/apache/maven-site-plugin/compare/maven-site-plugin-3.20.0...maven-site-plugin-3.21.0)

---
updated-dependencies:
- dependency-name: org.apache.maven.plugins:maven-site-plugin
  dependency-version: 3.21.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>
2025-12-29 11:23:08 +01:00
dependabot[bot] 07d5533cd6 build(deps): bump log4j2.version from 2.25.2 to 2.25.3 (#1486)
Bumps `log4j2.version` from 2.25.2 to 2.25.3.

Updates `org.apache.logging.log4j:log4j-api` from 2.25.2 to 2.25.3

Updates `org.apache.logging.log4j:log4j-core` from 2.25.2 to 2.25.3

Updates `org.apache.logging.log4j:log4j-jcl` from 2.25.2 to 2.25.3

Updates `org.apache.logging.log4j:log4j-slf4j-impl` from 2.25.2 to 2.25.3

---
updated-dependencies:
- dependency-name: org.apache.logging.log4j:log4j-api
  dependency-version: 2.25.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: org.apache.logging.log4j:log4j-core
  dependency-version: 2.25.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: org.apache.logging.log4j:log4j-jcl
  dependency-version: 2.25.3
  dependency-type: direct:development
  update-type: version-update:semver-patch
- dependency-name: org.apache.logging.log4j:log4j-slf4j-impl
  dependency-version: 2.25.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>
2025-12-23 09:36:21 +01:00
Ryan J Murphy b490e2c2fc Merge pull request #1445 from ryanmurf/multipartCleanup
WW-5573  Multipart stream file cleanup
2025-12-11 12:48:13 +01:00
Lukasz Lenart d5bff6aef8 Merge pull request #1466 from apache/dependabot/maven/release/struts-6-8-x/org.apache.maven.plugins-maven-failsafe-plugin-3.5.4
Bump org.apache.maven.plugins:maven-failsafe-plugin from 3.5.1 to 3.5.4
2025-12-11 12:47:41 +01:00
Lukasz Lenart 69ccd406fe Merge pull request #1462 from apache/dependabot/maven/release/struts-6-8-x/org.awaitility-awaitility-4.3.0
Bump org.awaitility:awaitility from 4.2.2 to 4.3.0
2025-12-11 12:47:21 +01:00
dependabot[bot] 9afee9994d Bump org.awaitility:awaitility from 4.2.2 to 4.3.0
Bumps [org.awaitility:awaitility](https://github.com/awaitility/awaitility) from 4.2.2 to 4.3.0.
- [Changelog](https://github.com/awaitility/awaitility/blob/master/changelog.txt)
- [Commits](https://github.com/awaitility/awaitility/compare/awaitility-4.2.2...awaitility-4.3.0)

---
updated-dependencies:
- dependency-name: org.awaitility:awaitility
  dependency-version: 4.3.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-11 10:44:01 +00:00
Lukasz Lenart 7d7060896b Merge pull request #1458 from apache/release/struts-6-7-x
Merges changes from older release branch
2025-12-11 11:33:22 +01:00
dependabot[bot] e37b68570f Bump org.apache.maven.plugins:maven-failsafe-plugin from 3.5.1 to 3.5.4
Bumps [org.apache.maven.plugins:maven-failsafe-plugin](https://github.com/apache/maven-surefire) from 3.5.1 to 3.5.4.
- [Release notes](https://github.com/apache/maven-surefire/releases)
- [Commits](https://github.com/apache/maven-surefire/compare/surefire-3.5.1...surefire-3.5.4)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-11 10:22:39 +00:00
dependabot[bot] 80739da117 Bump org.apache.commons:commons-lang3 from 3.17.0 to 3.20.0 (#1455)
Bumps org.apache.commons:commons-lang3 from 3.17.0 to 3.20.0.

---
updated-dependencies:
- dependency-name: org.apache.commons:commons-lang3
  dependency-version: 3.20.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>
2025-12-11 10:26:46 +01:00
dependabot[bot] c8b1d0e2f2 Bump org.apache.maven.plugins:maven-release-plugin from 3.1.1 to 3.3.0 (#1454)
Bumps [org.apache.maven.plugins:maven-release-plugin](https://github.com/apache/maven-release) from 3.1.1 to 3.3.0.
- [Release notes](https://github.com/apache/maven-release/releases)
- [Commits](https://github.com/apache/maven-release/compare/maven-release-3.1.1...maven-release-3.3.0)

---
updated-dependencies:
- dependency-name: org.apache.maven.plugins:maven-release-plugin
  dependency-version: 3.3.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>
2025-12-10 12:25:39 +01:00
dependabot[bot] e5d835bc19 Bump org.apache.maven.plugins:maven-enforcer-plugin from 3.5.0 to 3.6.2 (#1453)
Bumps [org.apache.maven.plugins:maven-enforcer-plugin](https://github.com/apache/maven-enforcer) from 3.5.0 to 3.6.2.
- [Release notes](https://github.com/apache/maven-enforcer/releases)
- [Commits](https://github.com/apache/maven-enforcer/compare/enforcer-3.5.0...enforcer-3.6.2)

---
updated-dependencies:
- dependency-name: org.apache.maven.plugins:maven-enforcer-plugin
  dependency-version: 3.6.2
  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>
2025-12-10 12:25:21 +01:00
dependabot[bot] 4997040be4 Bump maven-surefire-plugin.version from 3.5.1 to 3.5.4 (#1452)
Bumps `maven-surefire-plugin.version` from 3.5.1 to 3.5.4.

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

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

---
updated-dependencies:
- dependency-name: org.apache.maven.surefire:surefire-junit47
  dependency-version: 3.5.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: org.apache.maven.plugins:maven-surefire-plugin
  dependency-version: 3.5.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>
2025-12-10 12:25:02 +01:00
dependabot[bot] 5d648a62bf Bump org.apache.rat:apache-rat-plugin from 0.15 to 0.17 (#1436)
* Bump org.apache.rat:apache-rat-plugin from 0.15 to 0.17

Bumps org.apache.rat:apache-rat-plugin from 0.15 to 0.17.

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

Signed-off-by: dependabot[bot] <support@github.com>

* Updates exclusions

* Cleans up files with missing header

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Lukasz Lenart <lukaszlenart@apache.org>
2025-12-07 09:33:05 +01:00
dependabot[bot] 52dba77dc0 Bump org.jfree:jfreechart from 1.5.5 to 1.5.6 (#1440)
Bumps [org.jfree:jfreechart](https://github.com/jfree/jfreechart) from 1.5.5 to 1.5.6.
- [Release notes](https://github.com/jfree/jfreechart/releases)
- [Commits](https://github.com/jfree/jfreechart/compare/v1.5.5...v1.5.6)

---
updated-dependencies:
- dependency-name: org.jfree:jfreechart
  dependency-version: 1.5.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>
2025-12-01 06:55:32 +01:00
dependabot[bot] 1fcfc42ab2 Bump log4j2.version from 2.25.1 to 2.25.2 (#1437)
Bumps `log4j2.version` from 2.25.1 to 2.25.2.

Updates `org.apache.logging.log4j:log4j-api` from 2.25.1 to 2.25.2

Updates `org.apache.logging.log4j:log4j-core` from 2.25.1 to 2.25.2

Updates `org.apache.logging.log4j:log4j-jcl` from 2.25.1 to 2.25.2

Updates `org.apache.logging.log4j:log4j-slf4j-impl` from 2.25.1 to 2.25.2

---
updated-dependencies:
- dependency-name: org.apache.logging.log4j:log4j-api
  dependency-version: 2.25.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: org.apache.logging.log4j:log4j-core
  dependency-version: 2.25.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: org.apache.logging.log4j:log4j-jcl
  dependency-version: 2.25.2
  dependency-type: direct:development
  update-type: version-update:semver-patch
- dependency-name: org.apache.logging.log4j:log4j-slf4j-impl
  dependency-version: 2.25.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>
2025-12-01 06:54:13 +01:00
Lukasz Lenart cae08102d5 Removes unused jaxb-core dependency (#1434) 2025-11-28 08:44:15 +01:00
dependabot[bot] a810da1397 Bump com.sun.xml.bind:jaxb-core from 2.3.0.1 to 4.0.6 (#1429)
Bumps com.sun.xml.bind:jaxb-core from 2.3.0.1 to 4.0.6.

---
updated-dependencies:
- dependency-name: com.sun.xml.bind:jaxb-core
  dependency-version: 4.0.6
  dependency-type: direct:development
  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>
2025-11-26 19:07:26 +01:00
dependabot[bot] 0489abde40 Bump org.jacoco:jacoco-maven-plugin from 0.8.12 to 0.8.14 (#1428)
Bumps [org.jacoco:jacoco-maven-plugin](https://github.com/jacoco/jacoco) from 0.8.12 to 0.8.14.
- [Release notes](https://github.com/jacoco/jacoco/releases)
- [Commits](https://github.com/jacoco/jacoco/compare/v0.8.12...v0.8.14)

---
updated-dependencies:
- dependency-name: org.jacoco:jacoco-maven-plugin
  dependency-version: 0.8.14
  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>
2025-11-24 07:18:34 +01:00
dependabot[bot] 96006ab4ff Bump org.apache.maven.plugins:maven-dependency-plugin (#1426)
Bumps [org.apache.maven.plugins:maven-dependency-plugin](https://github.com/apache/maven-dependency-plugin) from 3.8.0 to 3.9.0.
- [Release notes](https://github.com/apache/maven-dependency-plugin/releases)
- [Commits](https://github.com/apache/maven-dependency-plugin/compare/maven-dependency-plugin-3.8.0...maven-dependency-plugin-3.9.0)

---
updated-dependencies:
- dependency-name: org.apache.maven.plugins:maven-dependency-plugin
  dependency-version: 3.9.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>
2025-11-24 07:18:05 +01:00
dependabot[bot] d9b84bc89e Bump jackson.version from 2.20.0 to 2.20.1 (#1412)
Bumps `jackson.version` from 2.20.0 to 2.20.1.

Updates `com.fasterxml.jackson.core:jackson-core` from 2.20.0 to 2.20.1
- [Commits](https://github.com/FasterXML/jackson-core/compare/jackson-core-2.20.0...jackson-core-2.20.1)

Updates `com.fasterxml.jackson.core:jackson-databind` from 2.20.0 to 2.20.1
- [Commits](https://github.com/FasterXML/jackson/commits)

Updates `com.fasterxml.jackson.dataformat:jackson-dataformat-xml` from 2.20.0 to 2.20.1
- [Commits](https://github.com/FasterXML/jackson-dataformat-xml/compare/jackson-dataformat-xml-2.20.0...jackson-dataformat-xml-2.20.1)

---
updated-dependencies:
- dependency-name: com.fasterxml.jackson.core:jackson-core
  dependency-version: 2.20.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: com.fasterxml.jackson.core:jackson-databind
  dependency-version: 2.20.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: com.fasterxml.jackson.dataformat:jackson-dataformat-xml
  dependency-version: 2.20.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>
2025-11-17 07:57:35 +01:00
dependabot[bot] 7003406d4c Bump org.codehaus.mojo:exec-maven-plugin from 3.4.1 to 3.6.2 (#1411)
Bumps [org.codehaus.mojo:exec-maven-plugin](https://github.com/mojohaus/exec-maven-plugin) from 3.4.1 to 3.6.2.
- [Release notes](https://github.com/mojohaus/exec-maven-plugin/releases)
- [Commits](https://github.com/mojohaus/exec-maven-plugin/compare/3.4.1...3.6.2)

---
updated-dependencies:
- dependency-name: org.codehaus.mojo:exec-maven-plugin
  dependency-version: 3.6.2
  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>
2025-11-17 07:56:47 +01:00
dependabot[bot] 8d935b1394 Bump com.thoughtworks.xstream:xstream from 1.4.20 to 1.4.21 (#1408)
Bumps [com.thoughtworks.xstream:xstream](https://github.com/x-stream/xstream) from 1.4.20 to 1.4.21.
- [Release notes](https://github.com/x-stream/xstream/releases)
- [Commits](https://github.com/x-stream/xstream/commits)

---
updated-dependencies:
- dependency-name: com.thoughtworks.xstream:xstream
  dependency-version: 1.4.21
  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>
2025-11-17 07:54:56 +01:00
dependabot[bot] f053c78db2 Bump asm.version from 9.7.1 to 9.9 (#1393)
Bumps `asm.version` from 9.7.1 to 9.9.

Updates `org.ow2.asm:asm` from 9.7.1 to 9.9

Updates `org.ow2.asm:asm-commons` from 9.7.1 to 9.9

---
updated-dependencies:
- dependency-name: org.ow2.asm:asm
  dependency-version: '9.9'
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: org.ow2.asm:asm-commons
  dependency-version: '9.9'
  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>
2025-10-27 06:51:55 +01:00
dependabot[bot] e805bb389f Bump org.freemarker:freemarker from 2.3.33 to 2.3.34 (#1386)
Bumps org.freemarker:freemarker from 2.3.33 to 2.3.34.

---
updated-dependencies:
- dependency-name: org.freemarker:freemarker
  dependency-version: 2.3.34
  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>
2025-10-27 06:49:41 +01:00
dependabot[bot] eb7a0804b8 Bump commons-validator:commons-validator from 1.9.0 to 1.10.0 (#1385)
Bumps commons-validator:commons-validator from 1.9.0 to 1.10.0.

---
updated-dependencies:
- dependency-name: commons-validator:commons-validator
  dependency-version: 1.10.0
  dependency-type: direct:development
  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>
2025-10-27 06:48:12 +01:00
dependabot[bot] e07612b762 Bump org.assertj:assertj-core from 3.27.4 to 3.27.6 (#1387)
Bumps [org.assertj:assertj-core](https://github.com/assertj/assertj) from 3.27.4 to 3.27.6.
- [Release notes](https://github.com/assertj/assertj/releases)
- [Commits](https://github.com/assertj/assertj/compare/assertj-build-3.27.4...assertj-build-3.27.6)

---
updated-dependencies:
- dependency-name: org.assertj:assertj-core
  dependency-version: 3.27.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>
2025-10-19 19:13:17 +02:00
Lukasz Lenart f92b2372bf Merge pull request #1369 from apache/dependabot/maven/release/struts-6-7-x/org.apache.maven.doxia-doxia-module-markdown-2.0.0
Bump org.apache.maven.doxia:doxia-module-markdown from 1.12.0 to 2.0.0
2025-09-29 08:23:33 +02:00
dependabot[bot] e5e535b01a Bump org.apache.maven.doxia:doxia-module-markdown from 1.12.0 to 2.0.0
Bumps org.apache.maven.doxia:doxia-module-markdown from 1.12.0 to 2.0.0.

---
updated-dependencies:
- dependency-name: org.apache.maven.doxia:doxia-module-markdown
  dependency-version: 2.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-29 01:36:07 +00:00
Lukasz Lenart 39c3df3032 Merge pull request #1356 from apache/dependabot/maven/release/struts-6-7-x/org.apache.maven.plugins-maven-failsafe-plugin-3.5.4
Bump org.apache.maven.plugins:maven-failsafe-plugin from 3.5.1 to 3.5.4
2025-09-28 11:51:33 +02:00
dependabot[bot] 730d553664 Bump org.apache.maven.plugins:maven-failsafe-plugin from 3.5.1 to 3.5.4
Bumps [org.apache.maven.plugins:maven-failsafe-plugin](https://github.com/apache/maven-surefire) from 3.5.1 to 3.5.4.
- [Release notes](https://github.com/apache/maven-surefire/releases)
- [Commits](https://github.com/apache/maven-surefire/compare/surefire-3.5.1...surefire-3.5.4)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-22 01:36:06 +00:00
Lukasz Lenart 34e279e6d7 [maven-release-plugin] prepare for next development iteration 2025-09-15 07:46:27 +02:00
170 changed files with 5757 additions and 1017 deletions
+11 -2
View File
@@ -24,11 +24,20 @@ github:
# it does not work because our github teams are private/secret, see INFRA-25666
require_code_owner_reviews: false
required_approving_review_count: 0
release/*:
support/struts-6-x-x:
# contexts are the names of checks that must pass.
required_status_checks:
contexts:
- "Build and Test (JDK 8)"
- "Build and Test (8)"
required_pull_request_reviews:
# it does not work because our github teams are private/secret, see INFRA-25666
require_code_owner_reviews: false
required_approving_review_count: 0
release/struts-6-*:
# contexts are the names of checks that must pass.
required_status_checks:
contexts:
- "Build and Test (8)"
required_pull_request_reviews:
# it does not work because our github teams are private/secret, see INFRA-25666
require_code_owner_reviews: false
+1
View File
@@ -18,6 +18,7 @@ name: "CodeQL"
on:
push:
branches:
- 'support/struts-6-x-x'
- 'release/*'
pull_request:
+2 -1
View File
@@ -19,7 +19,8 @@ on:
pull_request:
push:
branches:
- master
- 'support/struts-6-x-x'
- 'release/*'
permissions: read-all
+2 -1
View File
@@ -20,7 +20,8 @@ on:
schedule:
- cron: "30 1 * * 6" # Weekly on Saturdays
push:
branches: [ "master" ]
branches:
- 'main' # only default branch is supported
permissions: read-all
+6 -5
View File
@@ -19,7 +19,8 @@ on:
pull_request:
push:
branches:
- master
- 'support/struts-6-x-x'
- 'release/*'
permissions: read-all
@@ -31,12 +32,12 @@ jobs:
sonarcloud:
name: Scan
runs-on: ubuntu-latest
if: ${{ !github.event.pull_request.head.repo.fork }}
if: ${{ !github.event.pull_request.base.repo.fork && !github.event.pull_request.head.repo.fork && github.actor != 'dependabot[bot]' }}
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
with:
fetch-depth: 0
- uses: actions/setup-java@v4
- uses: actions/setup-java@v5
with:
distribution: temurin
java-version: 17
@@ -44,4 +45,4 @@ jobs:
- env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
SONAR_TOKEN: ${{ secrets.SONARCLOUD_TOKEN }}
run: mvn -B -V -Pcoverage -DskipAssembly verify org.sonarsource.scanner.maven:sonar-maven-plugin:sonar --no-transfer-progress
run: ./mvnw -B verify org.sonarsource.scanner.maven:sonar-maven-plugin:sonar -Pcoverage -DskipAssembly
+3
View File
@@ -49,3 +49,6 @@ test-output/
# Claude Code specific local settings
.claude/
.metals/
.bloop/
.vscode/
BIN
View File
Binary file not shown.
+4 -18
View File
@@ -1,18 +1,4 @@
# 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.
distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.6/apache-maven-3.9.6-bin.zip
wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar
wrapperVersion=3.3.4
distributionType=bin
distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.16/apache-maven-3.9.16-bin.zip
wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.3.4/maven-wrapper-3.3.4.jar
+23 -146
View File
@@ -1,160 +1,37 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Build Commands
## Apache Struts 2 Framework (Version 6.7.x)
This is the Apache Struts 2 web framework, a free open-source solution for creating Java web applications. The codebase is a multi-module Maven project with comprehensive plugin architecture.
## Build System & Common Commands
### Maven Commands
```bash
# Build entire project
./mvnw clean install
# Build without tests (fastest)
./mvnw clean install -DskipTests -DskipAssembly
# Build without running tests
./mvnw clean install -DskipTests
# Build without assembly
./mvnw clean install -DskipAssembly
# Run all tests
./mvnw clean test
# Run tests with coverage
./mvnw clean verify -Pcoverage -DskipAssembly
# Run integration tests
./mvnw clean verify -DskipAssembly
# Test specific module
# Test a single module
./mvnw -pl core clean test
./mvnw -pl plugins/spring clean test
# Check for security vulnerabilities
./mvnw clean verify -Pdependency-check
# Full build with tests
./mvnw clean install
# Run Apache RAT license check
./mvnw clean prepare-package
# Integration tests
./mvnw clean verify -DskipAssembly
# Coverage report
./mvnw clean verify -Pcoverage -DskipAssembly
```
### Test Framework
- **Primary**: JUnit 4.13.2 with Maven Surefire Plugin 3.5.1
- **Pattern**: `**/*Test.java` (excludes `**/TestBean.java`)
- **Coverage**: JaCoCo 0.8.12
- **Additional**: Mockito, EasyMock, AssertJ, Spring Test
- **Integration**: Maven Failsafe Plugin with Jetty on port 8090
## Project-Specific Rules
## Project Architecture
- This is the **6.x.x** branch (`release/struts-6-8-x`).
- Uses **javax.servlet** (Java EE), not Jakarta EE. Verify imports use `javax.servlet` namespace.
- Test pattern: `**/*Test.java`. Test classes use JUnit 4 with `@Test` annotations.
- OGNL expressions have strict security via `SecurityMemberAccess` — test any new OGNL usage against the security sandbox.
- Each plugin has its own `struts-plugin.xml` descriptor — register new beans there, not in core config.
- Run `./mvnw clean prepare-package` before committing to verify Apache RAT license headers pass.
### Core Structure
```
struts6/
├── core/ # Core Struts 2 framework (main dependency)
├── plugins/ # Plugin modules
│ ├── spring/ # Spring integration
│ ├── json/ # JSON support
│ ├── tiles/ # Apache Tiles integration
│ ├── velocity/ # Velocity template engine
│ └── [20+ other plugins]
├── apps/ # Sample applications
│ ├── showcase/ # Feature demonstration app
│ └── rest-showcase/ # REST API examples
├── bundles/ # OSGi bundles
├── bom/ # Bill of Materials
└── assembly/ # Distribution packaging
```
## Module Layout
### Key Technologies
- **Java**: Minimum JDK 8, supports up to JDK 21
- **Servlet API**: 3.1+ required
- **Expression Language**: OGNL 3.3.5
- **Dependency Injection**: Custom container (`com.opensymphony.xwork2.inject`)
- **Templating**: FreeMarker 2.3.33 (default), Velocity, JSP
- **Logging**: SLF4J 2.0.16 with Log4j2 2.24.1
- **Build**: Maven with wrapper (3.9.6)
### Core Components Architecture
#### Action Framework (MVC Pattern)
- **Actions**: Located in `core/src/main/java/org/apache/struts2/action/`
- **Action Support**: `ActionSupport` base class with validation and i18n
- **Action Context**: `ActionContext` provides access to servlet objects
- **Action Invocation**: `DefaultActionInvocation` handles action execution
#### Configuration System
- **XML-based**: Primary configuration via `struts.xml` files
- **Annotation-based**: Convention plugin for zero-config approach
- **Java-based**: `StrutsJavaConfiguration` for programmatic setup
- **Property files**: `struts.properties` for framework settings
#### Interceptor Chain
- **Framework Core**: All requests processed through interceptor chain
- **Built-in Interceptors**: 20+ interceptors in `org.apache.struts2.interceptor`
- **Validation**: `ValidationInterceptor` with annotation support
- **File Upload**: `FileUploadInterceptor` with security controls
- **Parameters**: `ParametersInterceptor` with OGNL expression handling
#### Result Framework
- **Result Types**: JSP, FreeMarker, Redirect, Stream, JSON, etc.
- **Chaining**: `ActionChainResult` for action-to-action calls
- **Templates**: Pluggable result renderers
#### Value Stack (OGNL Integration)
- **Expression Language**: OGNL for property access and method calls
- **Security**: `SecurityMemberAccess` prevents dangerous operations
- **Performance**: Caffeine-based expression caching
- **Context**: CompoundRoot provides hierarchical value resolution
### Plugin Architecture
Each plugin is a separate Maven module with:
- **Plugin Descriptor**: `struts-plugin.xml` defines beans and configuration
- **Dependency Isolation**: Separate classloaders for plugin resources
- **Extension Points**: Configurable via dependency injection
- **Popular Plugins**: Spring (DI), JSON (REST), Tiles (Layout), Bean Validation (JSR-303)
### Security Architecture
- **OGNL Security**: Restricted method access and class loading
- **CSRF Protection**: Token-based with `TokenInterceptor`
- **File Upload Security**: Type and size restrictions
- **Content Security Policy**: Built-in CSP support
- **Input Validation**: Server-side validation framework
- **Pattern Matching**: Configurable allowed/excluded patterns
## Development Guidelines
### Code Organization
- **Package Structure**: Follow existing `org.apache.struts2.*` hierarchy
- **Naming Conventions**: Use Struts conventions (Actions end with `Action`)
- **Configuration**: Prefer XML configuration in `struts.xml` for complex setups
- **Testing**: Each module has comprehensive unit and integration tests
### Plugin Development
```java
// Plugin descriptor example (struts-plugin.xml)
<bean type="com.opensymphony.xwork2.ObjectFactory"
name="myObjectFactory"
class="com.example.MyObjectFactory" />
```
### Common Patterns
- **Action Implementation**: Extend `ActionSupport` or implement `Action`
- **Result Mapping**: Use result configuration in `struts.xml`
- **Interceptor Development**: Extend `AbstractInterceptor`
- **Type Conversion**: Implement `TypeConverter` for custom types
- **Validation**: Use validation XML or annotations
### Important Notes
- **Version**: Currently 6.7.5-SNAPSHOT (release branch: `release/struts-6-7-x`)
- **Java Compatibility**: Compiled for Java 8, tested through Java 21
- **Security**: Always validate inputs and follow OWASP guidelines
- **Performance**: Leverage built-in caching (OGNL expressions, templates)
- **Deprecation**: Some legacy XWork components marked for removal
### Build Profiles
- **coverage**: Enables JaCoCo coverage reporting
- **dependency-check**: OWASP dependency vulnerability scanning
- **jdk17**: Special configuration for Java 17+ module system
This is a mature, enterprise-grade framework with extensive documentation at https://struts.apache.org/ and active community support through Apache mailing lists and JIRA (project WW).
- `core/` — framework core
- `plugins/` — 20+ plugin modules (spring, json, tiles, velocity, etc.)
- `apps/showcase/` — feature demo app
- `apps/rest-showcase/` — REST examples
Vendored
+22 -4
View File
@@ -1,4 +1,22 @@
#!groovy
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
pipeline {
agent none
@@ -81,7 +99,7 @@ pipeline {
stage('Code Quality') {
when {
anyOf {
branch 'release/struts-6-7-x'
branch 'support/struts-6-x-x'
}
}
steps {
@@ -127,7 +145,7 @@ pipeline {
}
stage('Build Source & JavaDoc') {
when {
branch 'release/struts-6-7-x'
branch 'support/struts-x-x-x'
}
steps {
dir("local-snapshots-dir/") {
@@ -138,7 +156,7 @@ pipeline {
}
stage('Deploy Snapshot') {
when {
branch 'release/struts-6-7-x'
branch 'support/struts-6-x-x'
}
steps {
withCredentials([file(credentialsId: 'lukaszlenart-repository-access-token', variable: 'CUSTOM_SETTINGS')]) {
@@ -148,7 +166,7 @@ pipeline {
}
stage('Upload nightlies') {
when {
branch 'release/struts-6-7-x'
branch 'support/struts-6-x-x'
}
steps {
sh './mvnw -B package -DskipTests'
+1 -1
View File
@@ -24,7 +24,7 @@
<parent>
<groupId>org.apache.struts</groupId>
<artifactId>struts2-parent</artifactId>
<version>6.8.0</version>
<version>6.12.0-SNAPSHOT</version>
</parent>
<artifactId>struts2-apps</artifactId>
<packaging>pom</packaging>
+2 -2
View File
@@ -24,12 +24,12 @@
<parent>
<groupId>org.apache.struts</groupId>
<artifactId>struts2-apps</artifactId>
<version>6.8.0</version>
<version>6.12.0-SNAPSHOT</version>
</parent>
<artifactId>struts2-rest-showcase</artifactId>
<packaging>war</packaging>
<version>6.8.0</version>
<version>6.12.0-SNAPSHOT</version>
<name>Struts 2 Rest Showcase Webapp</name>
<description>Struts 2 Rest Showcase Example</description>
+6 -2
View File
@@ -24,7 +24,7 @@
<parent>
<groupId>org.apache.struts</groupId>
<artifactId>struts2-apps</artifactId>
<version>6.8.0</version>
<version>6.12.0-SNAPSHOT</version>
</parent>
<artifactId>struts2-showcase</artifactId>
@@ -121,6 +121,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>opensymphony</groupId>
@@ -163,7 +167,7 @@
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-failsafe-plugin</artifactId>
<version>3.5.1</version>
<version>3.5.6</version>
<configuration>
<includes>
<include>it.org.apache.struts2.showcase.*Test</include>
+2 -2
View File
@@ -24,7 +24,7 @@
<parent>
<groupId>org.apache.struts</groupId>
<artifactId>struts2-parent</artifactId>
<version>6.8.0</version>
<version>6.12.0-SNAPSHOT</version>
</parent>
<artifactId>struts2-assembly</artifactId>
@@ -106,7 +106,7 @@
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-assembly-plugin</artifactId>
<version>3.7.1</version>
<version>3.8.0</version>
<executions>
<execution>
<id>make-assembly</id>
+3 -4
View File
@@ -25,11 +25,10 @@
<parent>
<groupId>org.apache.struts</groupId>
<artifactId>struts2-parent</artifactId>
<version>6.8.0</version>
<version>6.12.0-SNAPSHOT</version>
</parent>
<artifactId>struts2-bom</artifactId>
<version>6.8.0</version>
<packaging>pom</packaging>
<name>Struts 2 Bill of Materials</name>
@@ -44,7 +43,7 @@
</licenses>
<properties>
<struts-version.version>6.8.0</struts-version.version>
<struts-version.version>6.12.0-SNAPSHOT</struts-version.version>
<maven.site.skip>true</maven.site.skip>
<maven.site.deploy.skip>true</maven.site.deploy.skip>
</properties>
@@ -190,7 +189,7 @@
</dependencyManagement>
<scm>
<tag>STRUTS_6_8_0</tag>
<tag>STRUTS_6_9_0</tag>
<connection>scm:git:https://gitbox.apache.org/repos/asf/struts.git</connection>
<developerConnection>scm:git:https://gitbox.apache.org/repos/asf/struts.git</developerConnection>
<url>https://github.com/apache/struts/</url>
+1 -1
View File
@@ -24,7 +24,7 @@
<parent>
<groupId>org.apache.struts</groupId>
<artifactId>struts2-osgi-bundles</artifactId>
<version>6.8.0</version>
<version>6.12.0-SNAPSHOT</version>
</parent>
<artifactId>struts2-osgi-admin-bundle</artifactId>
+1 -1
View File
@@ -24,7 +24,7 @@
<parent>
<groupId>org.apache.struts</groupId>
<artifactId>struts2-osgi-bundles</artifactId>
<version>6.8.0</version>
<version>6.12.0-SNAPSHOT</version>
</parent>
<artifactId>struts2-osgi-demo-bundle</artifactId>
+1 -1
View File
@@ -24,7 +24,7 @@
<parent>
<groupId>org.apache.struts</groupId>
<artifactId>struts2-parent</artifactId>
<version>6.8.0</version>
<version>6.12.0-SNAPSHOT</version>
</parent>
<artifactId>struts2-osgi-bundles</artifactId>
+1 -1
View File
@@ -24,7 +24,7 @@
<parent>
<groupId>org.apache.struts</groupId>
<artifactId>struts2-parent</artifactId>
<version>6.8.0</version>
<version>6.12.0-SNAPSHOT</version>
</parent>
<artifactId>struts2-core</artifactId>
<packaging>jar</packaging>
@@ -168,8 +168,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;
}
}
@@ -19,7 +19,25 @@
package com.opensymphony.xwork2.config;
/**
* When implemented allows to alias already existing beans
* A {@link ConfigurationProvider} that selects and aliases bean implementations.
* <p>
* Implementations of this interface are responsible for selecting which bean implementation
* to use for a given interface type. The selection is typically based on configuration properties
* that specify the bean name or class name.
* </p>
* <p>
* The aliasing mechanism works as follows:
* </p>
* <ol>
* <li>Look for a bean by the name specified in the configuration property</li>
* <li>If found, alias it to the default name so it becomes the default implementation</li>
* <li>If not found, try to load the value as a class name and register it as a factory</li>
* <li>If class loading fails, delegate to {@link org.apache.struts2.ObjectFactory} at runtime
* (useful for Spring bean names)</li>
* </ol>
*
* @see AbstractBeanSelectionProvider
* @see StrutsBeanSelectionProvider
*/
public interface BeanSelectionProvider extends ConfigurationProvider {
@@ -82,6 +82,8 @@ import com.opensymphony.xwork2.ognl.BeanInfoCacheFactory;
import com.opensymphony.xwork2.ognl.DefaultOgnlBeanInfoCacheFactory;
import com.opensymphony.xwork2.ognl.DefaultOgnlExpressionCacheFactory;
import com.opensymphony.xwork2.ognl.ExpressionCacheFactory;
import com.opensymphony.xwork2.ognl.ProxyCacheFactory;
import com.opensymphony.xwork2.ognl.StrutsProxyCacheFactory;
import com.opensymphony.xwork2.ognl.OgnlCacheFactory;
import com.opensymphony.xwork2.ognl.OgnlReflectionProvider;
import com.opensymphony.xwork2.ognl.OgnlUtil;
@@ -93,6 +95,7 @@ import com.opensymphony.xwork2.ognl.accessor.XWorkMethodAccessor;
import com.opensymphony.xwork2.util.OgnlTextParser;
import com.opensymphony.xwork2.util.PatternMatcher;
import com.opensymphony.xwork2.util.StrutsLocalizedTextProvider;
import com.opensymphony.xwork2.util.StrutsProxyCacheFactoryBean;
import com.opensymphony.xwork2.util.TextParser;
import com.opensymphony.xwork2.util.ValueStack;
import com.opensymphony.xwork2.util.ValueStackFactory;
@@ -106,6 +109,8 @@ import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.apache.struts2.StrutsConstants;
import org.apache.struts2.conversion.StrutsConversionPropertiesProcessor;
import org.apache.struts2.conversion.UserConversionPropertiesProcessor;
import org.apache.struts2.conversion.UserConversionPropertiesProvider;
import org.apache.struts2.conversion.StrutsTypeConverterCreator;
import org.apache.struts2.conversion.StrutsTypeConverterHolder;
import org.apache.struts2.factory.StrutsResultFactory;
@@ -125,12 +130,8 @@ import java.util.Set;
import java.util.TreeMap;
import java.util.TreeSet;
/**
* DefaultConfiguration
*
* @author Jason Carreira
* Created Feb 24, 2003 7:38:06 AM
*/
public class DefaultConfiguration implements Configuration {
@@ -145,6 +146,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);
}
@@ -224,7 +227,7 @@ public class DefaultConfiguration implements Configuration {
name, packageContext.getLocation());
} else {
throw new ConfigurationException("The package name '" + name
+ "' at location "+packageContext.getLocation()
+ "' at location " + packageContext.getLocation()
+ " is already been used by another package at location " + check.getLocation(),
packageContext);
}
@@ -257,7 +260,6 @@ public class DefaultConfiguration implements Configuration {
*
* @param providers list of ContainerProvider
* @return list of package providers
*
* @throws ConfigurationException in case of any configuration errors
*/
@Override
@@ -269,8 +271,7 @@ public class DefaultConfiguration implements Configuration {
ContainerProperties props = new ContainerProperties();
ContainerBuilder builder = new ContainerBuilder();
Container bootstrap = createBootstrapContainer(providers);
for (final ContainerProvider containerProvider : providers)
{
for (final ContainerProvider containerProvider : providers) {
bootstrap.inject(containerProvider);
containerProvider.init(this);
containerProvider.register(builder, props);
@@ -298,13 +299,16 @@ public class DefaultConfiguration implements Configuration {
setContext(container);
objectFactory = container.getInstance(ObjectFactory.class);
// Trigger late initialization of user conversion properties (WW-4291)
// This must happen after full container is built so SpringObjectFactory is available
container.getInstance(UserConversionPropertiesProcessor.class);
// Process the configuration providers first
for (final ContainerProvider containerProvider : providers)
{
for (final ContainerProvider containerProvider : providers) {
if (containerProvider instanceof PackageProvider) {
container.inject(containerProvider);
((PackageProvider)containerProvider).loadPackages();
packageProviders.add((PackageProvider)containerProvider);
((PackageProvider) containerProvider).loadPackages();
packageProviders.add((PackageProvider) containerProvider);
}
}
@@ -380,6 +384,8 @@ public class DefaultConfiguration implements Configuration {
.factory(ConversionAnnotationProcessor.class, DefaultConversionAnnotationProcessor.class, Scope.SINGLETON)
.factory(TypeConverterCreator.class, StrutsTypeConverterCreator.class, Scope.SINGLETON)
.factory(TypeConverterHolder.class, StrutsTypeConverterHolder.class, Scope.SINGLETON)
.factory(UserConversionPropertiesProvider.class, StrutsConversionPropertiesProcessor.class, Scope.SINGLETON)
.factory(UserConversionPropertiesProcessor.class, Scope.SINGLETON)
.factory(TextProvider.class, "system", DefaultTextProvider.class, Scope.SINGLETON)
.factory(LocalizedTextProvider.class, StrutsLocalizedTextProvider.class, Scope.SINGLETON)
@@ -393,6 +399,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(StrutsProxyCacheFactoryBean.class, Scope.SINGLETON)
.factory(OgnlUtil.class, Scope.SINGLETON)
.factory(SecurityMemberAccess.class, Scope.PROTOTYPE)
.factory(OgnlGuard.class, StrutsOgnlGuard.class, Scope.SINGLETON)
@@ -443,10 +451,9 @@ public class DefaultConfiguration implements Configuration {
Map<String, ActionConfig> actionConfigs = packageConfig.getAllActionConfigs();
for (Object o : actionConfigs.keySet()) {
String actionName = (String) o;
ActionConfig baseConfig = actionConfigs.get(actionName);
configs.put(actionName, buildFullActionConfig(packageConfig, baseConfig));
for (Map.Entry<String, ActionConfig> entry : actionConfigs.entrySet()) {
ActionConfig baseConfig = entry.getValue();
configs.put(entry.getKey(), buildFullActionConfig(packageConfig, baseConfig));
}
namespaceActionConfigs.put(namespace, configs);
@@ -487,8 +494,7 @@ public class DefaultConfiguration implements Configuration {
* @param baseConfig the ActionConfig which holds only the configuration specific to itself, without the defaults
* and inheritance
* @return a full ActionConfig for runtime configuration with all of the inherited and default params
* @throws com.opensymphony.xwork2.config.ConfigurationException
*
* @throws com.opensymphony.xwork2.config.ConfigurationException in case of any configuration errors
*/
private ActionConfig buildFullActionConfig(PackageConfig packageContext, ActionConfig baseConfig) throws ConfigurationException {
Map<String, String> params = new TreeMap<>(baseConfig.getParams());
@@ -500,7 +506,7 @@ public class DefaultConfiguration implements Configuration {
results.putAll(packageContext.getAllGlobalResults());
}
results.putAll(baseConfig.getResults());
results.putAll(baseConfig.getResults());
setDefaultResults(results, packageContext);
@@ -511,7 +517,7 @@ public class DefaultConfiguration implements Configuration {
if (defaultInterceptorRefName != null) {
interceptors.addAll(InterceptorBuilder.constructInterceptorReference(new PackageConfig.Builder(packageContext), defaultInterceptorRefName,
new LinkedHashMap<String, String>(), packageContext.getLocation(), objectFactory));
new LinkedHashMap<>(), packageContext.getLocation(), objectFactory));
}
}
@@ -523,14 +529,14 @@ public class DefaultConfiguration implements Configuration {
LOG.debug("Using pattern [{}] to match allowed methods when SMI is disabled!", methodRegex);
return new ActionConfig.Builder(baseConfig)
.addParams(params)
.addResultConfigs(results)
.defaultClassName(packageContext.getDefaultClassRef()) // fill in default if non class has been provided
.interceptors(interceptors)
.setStrictMethodInvocation(packageContext.isStrictMethodInvocation())
.setDefaultMethodRegex(methodRegex)
.addExceptionMappings(packageContext.getAllExceptionMappingConfigs())
.build();
.addParams(params)
.addResultConfigs(results)
.defaultClassName(packageContext.getDefaultClassRef()) // fill in default if non class has been provided
.interceptors(interceptors)
.setStrictMethodInvocation(packageContext.isStrictMethodInvocation())
.setDefaultMethodRegex(methodRegex)
.addExceptionMappings(packageContext.getAllExceptionMappingConfigs())
.build();
}
@@ -546,8 +552,7 @@ public class DefaultConfiguration implements Configuration {
Map<String, String> namespaceConfigs,
PatternMatcher<int[]> matcher,
boolean appendNamedParameters,
boolean fallbackToEmptyNamespace)
{
boolean fallbackToEmptyNamespace) {
this.namespaceActionConfigs = namespaceActionConfigs;
this.namespaceConfigs = namespaceConfigs;
this.fallbackToEmptyNamespace = fallbackToEmptyNamespace;
@@ -617,6 +622,9 @@ public class DefaultConfiguration implements Configuration {
String defaultActionRef = namespaceConfigs.get(namespace);
if (defaultActionRef != null) {
config = actions.get(defaultActionRef);
if (config == null) {
config = namespaceActionConfigMatchers.get(namespace).match(defaultActionRef);
}
}
}
}
@@ -630,7 +638,7 @@ public class DefaultConfiguration implements Configuration {
* @return a Map of namespace - > Map of ActionConfig objects, with the key being the action name
*/
@Override
public Map<String, Map<String, ActionConfig>> getActionConfigs() {
public Map<String, Map<String, ActionConfig>> getActionConfigs() {
return namespaceActionConfigs;
}
@@ -664,7 +672,7 @@ public class DefaultConfiguration implements Configuration {
public void setConstants(ContainerBuilder builder) {
for (Object keyobj : keySet()) {
String key = (String)keyobj;
String key = (String) keyobj;
builder.factory(String.class, key, new LocatableConstantFactory<>(getProperty(key), getPropertyLocation(key)));
}
}
@@ -61,7 +61,7 @@ public class CollectionConverter extends DefaultTypeConverter {
for (Object anObjArray : objArray) {
Object convertedValue = converter.convertValue(context, target, member, propertyName, anObjArray, memberType);
if (!TypeConverter.NO_CONVERSION_POSSIBLE.equals(convertedValue)) {
if (convertedValue != TypeConverter.NO_CONVERSION_POSSIBLE) {
result.add(convertedValue);
}
}
@@ -72,7 +72,7 @@ public class CollectionConverter extends DefaultTypeConverter {
for (Object aCol : col) {
Object convertedValue = converter.convertValue(context, target, member, propertyName, aCol, memberType);
if (!TypeConverter.NO_CONVERSION_POSSIBLE.equals(convertedValue)) {
if (convertedValue != TypeConverter.NO_CONVERSION_POSSIBLE) {
result.add(convertedValue);
}
}
@@ -80,7 +80,7 @@ public class CollectionConverter extends DefaultTypeConverter {
result = createCollection(toType, memberType, -1);
TypeConverter converter = getTypeConverter(context);
Object convertedValue = converter.convertValue(context, target, member, propertyName, value, memberType);
if (!TypeConverter.NO_CONVERSION_POSSIBLE.equals(convertedValue)) {
if (convertedValue != TypeConverter.NO_CONVERSION_POSSIBLE) {
result.add(convertedValue);
}
}
@@ -36,6 +36,18 @@ import java.util.Objects;
public class StringConverter extends DefaultTypeConverter {
/**
* Upper bound on the number of fraction digits emitted when formatting a number.
* <p>
* Covers every {@code double} and {@code float} value in full - the widest is
* {@link Double#MIN_VALUE} at 325 fraction digits - so the round-trip precision
* introduced by WW-4871 is preserved. Beyond that bound the length of the output
* would follow the scale of the value rather than its precision, so a
* {@link BigDecimal} scaled past this limit is rounded to it.
*/
private static final int MAX_FRACTION_DIGITS = 340;
@Override
public Object convertValue(Map<String, Object> context, Object target, Member member, String propertyName, Object value, Class toType) {
String result;
@@ -86,7 +98,7 @@ public class StringConverter extends DefaultTypeConverter {
// TODO: delete this variable and corresponding if statement when jdk fixed java.text.NumberFormat.format's behavior with Float
Object fixedValue = value;
if (BigDecimal.class.isInstance(value) || Double.class.isInstance(value) || Float.class.isInstance(value)) {
format.setMaximumFractionDigits(Integer.MAX_VALUE);
format.setMaximumFractionDigits(MAX_FRACTION_DIGITS);
if (Float.class.isInstance(value)) {
fixedValue = Double.valueOf(value.toString());
}
@@ -26,7 +26,7 @@ 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());
@@ -45,22 +45,49 @@ class FinalizableReferenceQueue extends ReferenceQueue<Object> {
logger.log(Level.SEVERE, "Error cleaning up after reference.", t);
}
private volatile Thread drainThread;
void start() {
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) {
break;
}
}
}
};
thread.setDaemon(true);
thread.start();
this.drainThread = thread;
}
static ReferenceQueue<Object> instance = createAndStart();
/**
* Stops the background drain thread and releases the singleton instance,
* preventing the webapp classloader from being pinned after undeploy.
*/
public static synchronized void stopAndClear() {
if (instance instanceof FinalizableReferenceQueue) {
FinalizableReferenceQueue queue = (FinalizableReferenceQueue) instance;
Thread t = queue.drainThread;
if (t != null) {
t.interrupt();
try {
t.join(5000);
} catch (InterruptedException ignored) {
Thread.currentThread().interrupt();
}
t.setContextClassLoader(null);
queue.drainThread = null;
}
}
instance = null;
}
static volatile ReferenceQueue<Object> instance = createAndStart();
static FinalizableReferenceQueue createAndStart() {
FinalizableReferenceQueue queue = new FinalizableReferenceQueue();
@@ -98,8 +98,8 @@ public abstract class MethodFilterInterceptor extends AbstractInterceptor {
@Override
public String intercept(ActionInvocation invocation) throws Exception {
if (applyInterceptor(invocation)) {
return doIntercept(invocation);
if (applyInterceptor((org.apache.struts2.ActionInvocation) invocation)) {
return doIntercept((org.apache.struts2.ActionInvocation) invocation);
}
return invocation.invoke();
}
@@ -114,6 +114,10 @@ public abstract class MethodFilterInterceptor extends AbstractInterceptor {
return applyMethod;
}
protected boolean applyInterceptor(org.apache.struts2.ActionInvocation invocation) {
return applyInterceptor(ActionInvocation.adapt(invocation));
}
/**
* Subclasses must override to implement the interceptor logic.
*
@@ -123,4 +127,7 @@ public abstract class MethodFilterInterceptor extends AbstractInterceptor {
*/
protected abstract String doIntercept(ActionInvocation invocation) throws Exception;
protected String doIntercept(org.apache.struts2.ActionInvocation invocation) throws Exception {
return doIntercept(ActionInvocation.adapt(invocation));
}
}
@@ -31,6 +31,15 @@ public interface OgnlCache<Key, Value> {
void putIfAbsent(Key key, Value value);
/**
* Removes the mapping for the given key, if present.
*
* @param key the key to remove
* @return the previous value associated with the key, or {@code null} if none
* @since 6.11.0
*/
Value remove(Key key);
int size();
void clear();
@@ -56,6 +56,11 @@ public class OgnlCaffeineCache<K, V> implements OgnlCache<K, V> {
cache.asMap().putIfAbsent(key, value);
}
@Override
public V remove(K key) {
return cache.asMap().remove(key);
}
@Override
public int size() {
return cache.asMap().size();
@@ -57,6 +57,11 @@ public class OgnlDefaultCache<K, V> implements OgnlCache<K, V> {
this.clearIfEvictionLimitExceeded();
}
@Override
public V remove(K key) {
return ognlCache.remove(key);
}
@Override
public int size() {
return ognlCache.size();
@@ -64,6 +64,11 @@ public class OgnlLRUCache<K, V> implements OgnlCache<K, V> {
ognlLRUCache.putIfAbsent(key, value);
}
@Override
public V remove(K key) {
return ognlLRUCache.remove(key);
}
@Override
public int size() {
return ognlLRUCache.size();
@@ -0,0 +1,31 @@
/*
* 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 com.opensymphony.xwork2.ognl;
/**
* A factory interface for ProxyUtil cache to be used with Struts DI mechanism.
* This allows the proxy detection cache type to be configurable via Struts constants.
*
* @param <Key> The type for the cache key entries
* @param <Value> The type for the cache value entries
* @since 6.9.0
*/
public interface ProxyCacheFactory<Key, Value> extends OgnlCacheFactory<Key, Value> {
}
@@ -0,0 +1,44 @@
/*
* 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 com.opensymphony.xwork2.ognl;
import com.opensymphony.xwork2.inject.Inject;
import org.apache.commons.lang3.EnumUtils;
import org.apache.struts2.StrutsConstants;
/**
* Struts Proxy Cache factory implementation for ProxyUtil caches.
* <p>
* This factory is used to create caches for proxy detection in ProxyUtil.
* The cache type and size can be configured via Struts constants.
*
* @param <Key> The type for the cache key entries
* @param <Value> The type for the cache value entries
* @since 6.9.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));
}
}
@@ -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.dispatcher.InternalDestroyable;
import java.beans.IntrospectionException;
import java.beans.PropertyDescriptor;
@@ -53,7 +54,7 @@ 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
@@ -74,6 +75,22 @@ public class CompoundRootAccessor implements RootAccessor {
private final static Logger LOG = LogManager.getLogger(CompoundRootAccessor.class);
private final static Class[] EMPTY_CLASS_ARRAY = new Class[0];
private static final Map<MethodCall, Boolean> invalidMethods = new ConcurrentHashMap<>();
/**
* Clears the cached invalid methods map to prevent classloader leaks on hot redeploy.
*/
public static void clearCache() {
invalidMethods.clear();
}
/**
* @since 6.9.0
*/
@Override
public void destroy() {
clearCache();
}
private boolean devMode;
private boolean disallowCustomOgnlMap;
@@ -20,6 +20,7 @@ package com.opensymphony.xwork2.ognl.accessor;
import com.opensymphony.xwork2.ObjectFactory;
import com.opensymphony.xwork2.conversion.ObjectTypeDeterminer;
import com.opensymphony.xwork2.conversion.TypeConverter;
import com.opensymphony.xwork2.conversion.impl.XWorkConverter;
import com.opensymphony.xwork2.inject.Inject;
import com.opensymphony.xwork2.ognl.OgnlUtil;
@@ -29,6 +30,8 @@ import ognl.OgnlException;
import ognl.PropertyAccessor;
import org.apache.struts2.StrutsConstants;
import org.apache.struts2.StrutsException;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import java.util.Collection;
import java.util.List;
@@ -43,6 +46,8 @@ import java.util.Map;
*/
public class XWorkListPropertyAccessor extends ListPropertyAccessor {
private static final Logger LOG = LogManager.getLogger(XWorkListPropertyAccessor.class);
private XWorkCollectionPropertyAccessor _sAcc = new XWorkCollectionPropertyAccessor();
private XWorkConverter xworkConverter;
@@ -167,6 +172,10 @@ public class XWorkListPropertyAccessor extends ListPropertyAccessor {
}
Object realValue = getRealValue(context, value, convertToClass);
if (realValue == TypeConverter.NO_CONVERSION_POSSIBLE) {
LOG.debug("Unable to convert value for index [{}] to the declared element type, skipping assignment", name);
return;
}
if (target instanceof List && name instanceof Number) {
//make sure there are enough spaces in the List to set
@@ -20,6 +20,7 @@ package com.opensymphony.xwork2.ognl.accessor;
import com.opensymphony.xwork2.ObjectFactory;
import com.opensymphony.xwork2.conversion.ObjectTypeDeterminer;
import com.opensymphony.xwork2.conversion.TypeConverter;
import com.opensymphony.xwork2.conversion.impl.XWorkConverter;
import com.opensymphony.xwork2.inject.Inject;
import com.opensymphony.xwork2.util.reflection.ReflectionContextState;
@@ -126,8 +127,17 @@ public class XWorkMapPropertyAccessor extends MapPropertyAccessor {
LOG.trace("Entering setProperty({},{},{},{})", context, target, name, value);
Object key = getKey(context, name);
if (key == TypeConverter.NO_CONVERSION_POSSIBLE) {
LOG.debug("Unable to convert key [{}] to the declared key type, skipping assignment", name);
return;
}
Object convertedValue = getValue(context, value);
if (convertedValue == TypeConverter.NO_CONVERSION_POSSIBLE) {
LOG.debug("Unable to convert value for key [{}] to the declared element type, skipping assignment", key);
return;
}
Map map = (Map) target;
map.put(key, getValue(context, value));
map.put(key, convertedValue);
}
private Object getValue(Map context, Object value) {
@@ -21,6 +21,10 @@ package com.opensymphony.xwork2.util;
import com.opensymphony.xwork2.ActionContext;
import com.opensymphony.xwork2.LocalizedTextProvider;
import com.opensymphony.xwork2.inject.Inject;
import com.opensymphony.xwork2.ognl.DefaultOgnlCacheFactory;
import com.opensymphony.xwork2.ognl.OgnlCache;
import com.opensymphony.xwork2.ognl.OgnlCacheFactory.CacheType;
import org.apache.commons.lang3.EnumUtils;
import org.apache.commons.lang3.ObjectUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
@@ -45,6 +49,11 @@ import java.util.concurrent.CopyOnWriteArrayList;
abstract class AbstractLocalizedTextProvider implements LocalizedTextProvider {
// Pinned to the value implicitly computed for the pre-6.11.0 class shape, so sessions serialized by
// an older node still deserialize here during a rolling upgrade. The caches this change made transient
// are simply discarded from such a stream and rebuilt by readObject.
private static final long serialVersionUID = -4563130226985473584L;
private static final Logger LOG = LogManager.getLogger(AbstractLocalizedTextProvider.class);
public static final String XWORK_MESSAGES_BUNDLE = "com/opensymphony/xwork2/xwork-messages";
@@ -56,16 +65,37 @@ abstract class AbstractLocalizedTextProvider implements LocalizedTextProvider {
private static final String TOMCAT_WEBAPP_CLASSLOADER_BASE = "org.apache.catalina.loader.WebappClassLoaderBase";
private static final String RELOADED = "com.opensymphony.xwork2.util.LocalizedTextProvider.reloaded";
protected final ConcurrentMap<String, ResourceBundle> bundlesMap = new ConcurrentHashMap<>();
protected boolean devMode = false;
protected boolean reloadBundles = false;
protected boolean searchDefaultBundlesFirst = false; // Search default resource bundles first. Note: This flag may not be meaningful to all implementations.
private final ConcurrentMap<MessageFormatKey, MessageFormat> messageFormats = new ConcurrentHashMap<>();
private final ConcurrentMap<Integer, List<String>> classLoaderMap = new ConcurrentHashMap<>();
private final Set<String> missingBundles = ConcurrentHashMap.newKeySet();
private final ConcurrentMap<Integer, ClassLoader> delegatedClassLoaderMap = new ConcurrentHashMap<>();
// Dedicated monitor for bundlesMap-related synchronization: bundlesMap is reassigned by
// rebuildI18nCaches(), so locking on it directly would lock on a monitor that can change identity.
// transient + reinitialised in readObject: a bare Object is not Serializable.
private transient Object bundlesMapLock = new Object();
private static final int DEFAULT_I18N_CACHE_MAX_SIZE = 10000;
private volatile CacheType i18nCacheType = CacheType.WTLFU;
private volatile int i18nCacheMaxSize = DEFAULT_I18N_CACHE_MAX_SIZE;
private <K, V> OgnlCache<K, V> buildI18nCache() {
return new DefaultOgnlCacheFactory<K, V>(i18nCacheMaxSize, i18nCacheType).buildOgnlCache();
}
// The OgnlCache implementations are themselves thread-safe; volatile only safely publishes the
// reference when rebuildI18nCaches() replaces a cache (during injection / readObject), so S3077
// ("volatile is not enough") does not apply here.
@SuppressWarnings("java:S3077")
protected transient volatile OgnlCache<String, ResourceBundle> bundlesMap = buildI18nCache();
@SuppressWarnings("java:S3077")
private transient volatile OgnlCache<MessageFormatKey, MessageFormat> messageFormats = buildI18nCache();
@SuppressWarnings("java:S3077")
private transient volatile OgnlCache<String, Boolean> missingBundles = buildI18nCache();
/**
* Adds the bundle to the internal list of default bundles.
* If the bundle already exists in the list it will be re-added.
@@ -99,6 +129,21 @@ abstract class AbstractLocalizedTextProvider implements LocalizedTextProvider {
return Thread.currentThread().getContextClassLoader();
}
/** Test-support accessor: current number of cached resource bundles. */
protected int bundlesMapSize() {
return bundlesMap.size();
}
/** Test-support accessor: current number of cached missing-bundle markers. */
protected int missingBundlesSize() {
return missingBundles.size();
}
/** Test-support accessor: current number of cached message formats. */
protected int messageFormatsSize() {
return messageFormats.size();
}
@Inject(value = StrutsConstants.STRUTS_CUSTOM_I18N_RESOURCES, required = false)
public void setCustomI18NResources(String bundles) {
if (bundles != null && bundles.length() > 0) {
@@ -221,7 +266,7 @@ abstract class AbstractLocalizedTextProvider implements LocalizedTextProvider {
* @param classLoader a {@link ClassLoader} to look up the bundle from if none can be found on the current thread's classloader
*/
public void setDelegatedClassLoader(final ClassLoader classLoader) {
synchronized (bundlesMap) {
synchronized (bundlesMapLock) {
delegatedClassLoaderMap.put(getCurrentThreadContextClassLoader().hashCode(), classLoader);
}
}
@@ -443,6 +488,52 @@ abstract class AbstractLocalizedTextProvider implements LocalizedTextProvider {
this.searchDefaultBundlesFirst = Boolean.parseBoolean(searchDefaultBundlesFirst);
}
/**
* @param cacheType the type of cache to use for the localized-text caches
*
* @since 6.11.0
*/
@Inject(value = StrutsConstants.STRUTS_I18N_CACHE_TYPE, required = false)
public void setI18nCacheType(String cacheType) {
this.i18nCacheType = EnumUtils.getEnumIgnoreCase(CacheType.class, cacheType, CacheType.WTLFU);
rebuildI18nCaches();
}
/**
* @param cacheMaxSize the maximum size of each localized-text cache
*
* @since 6.11.0
*/
@Inject(value = StrutsConstants.STRUTS_I18N_CACHE_MAXSIZE, required = false)
public void setI18nCacheMaxSize(String cacheMaxSize) {
this.i18nCacheMaxSize = Integer.parseInt(cacheMaxSize);
rebuildI18nCaches();
}
/**
* Rebuilds the localized-text caches from the current type/size. Called during dependency injection
* (single-threaded startup, before the provider serves lookups); discards any warm-up entries.
*/
private void rebuildI18nCaches() {
bundlesMap = buildI18nCache();
messageFormats = buildI18nCache();
missingBundles = buildI18nCache();
}
private void readObject(java.io.ObjectInputStream in) throws java.io.IOException, ClassNotFoundException {
in.defaultReadObject();
bundlesMapLock = new Object();
// Field initialisers do not run during deserialization, so a stream written before these settings
// existed (an older node in a rolling upgrade) leaves them at null/0. Restore the defaults.
if (i18nCacheType == null) {
i18nCacheType = CacheType.WTLFU;
}
if (i18nCacheMaxSize <= 0) {
i18nCacheMaxSize = DEFAULT_I18N_CACHE_MAX_SIZE;
}
rebuildI18nCaches();
}
/**
* Finds the given resource bundle by it's name.
* <p>
@@ -458,34 +549,32 @@ abstract class AbstractLocalizedTextProvider implements LocalizedTextProvider {
ClassLoader classLoader = getCurrentThreadContextClassLoader();
String key = createMissesKey(String.valueOf(classLoader.hashCode()), aBundleName, locale);
if (missingBundles.contains(key)) {
if (missingBundles.get(key) != null) {
return null;
}
ResourceBundle bundle = null;
try {
if (bundlesMap.containsKey(key)) {
bundle = bundlesMap.get(key);
} else {
bundle = bundlesMap.get(key);
if (bundle == null) {
bundle = ResourceBundle.getBundle(aBundleName, locale, classLoader);
bundlesMap.putIfAbsent(key, bundle);
}
} catch (MissingResourceException ex) {
if (delegatedClassLoaderMap.containsKey(classLoader.hashCode())) {
try {
if (bundlesMap.containsKey(key)) {
bundle = bundlesMap.get(key);
} else {
bundle = bundlesMap.get(key);
if (bundle == null) {
bundle = ResourceBundle.getBundle(aBundleName, locale, delegatedClassLoaderMap.get(classLoader.hashCode()));
bundlesMap.putIfAbsent(key, bundle);
}
} catch (MissingResourceException e) {
LOG.debug("Missing resource bundle [{}]!", aBundleName, e);
missingBundles.add(key);
missingBundles.put(key, Boolean.TRUE);
}
} else {
LOG.debug("Missing resource bundle [{}]!", aBundleName);
missingBundles.add(key);
missingBundles.put(key, Boolean.TRUE);
}
}
return bundle;
@@ -18,6 +18,8 @@
*/
package com.opensymphony.xwork2.util;
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.opensymphony.xwork2.config.ConfigurationException;
import com.opensymphony.xwork2.ognl.OgnlUtil;
@@ -33,6 +35,17 @@ import static java.util.stream.Collectors.toSet;
import static org.apache.commons.lang3.StringUtils.strip;
public class ConfigParseUtil {
// Size the cache to prevent excessive memory usage in environments with many classloaders and/or large numbers of classes being validated.
// While still providing a reasonable caching benefit for common cases (e.g. multiple Struts instances in the same container, or multiple calls to validate the same class across different containers).
// The cache is sized to allow for some level of caching across multiple classloaders, while still allowing for a reasonable number of classes to be cached per classloader.
private static final int MAX_CLASSLOADER_CACHE_SIZE = 25;
// The cache for validated classes is a two-level cache, with the first level keyed by ClassLoader and the second level keyed by class name.
private static final int MAX_CLASS_CACHE_PER_LOADER_SIZE = 50;
private static final Cache<ClassLoader, Cache<String, Class<?>>> VALIDATED_CLASS_CACHE = Caffeine.newBuilder()
.weakKeys()
.maximumSize(MAX_CLASSLOADER_CACHE_SIZE)
.build();
private ConfigParseUtil() {
}
@@ -73,7 +86,7 @@ public class ConfigParseUtil {
Set<Class<?>> classes = new HashSet<>();
for (String className : classNames) {
try {
classes.add(validatingClassLoader.loadClass(className));
classes.add(loadAndCacheClass(validatingClassLoader, className));
} catch (ClassNotFoundException e) {
throw new ConfigurationException("Cannot load class for exclusion/exemption configuration: " + className, e);
}
@@ -81,6 +94,35 @@ public class ConfigParseUtil {
return classes;
}
private static Class<?> loadAndCacheClass(ClassLoader validatingClassLoader, String className) throws ClassNotFoundException {
Cache<String, Class<?>> classLoaderCache = VALIDATED_CLASS_CACHE.get(validatingClassLoader,
key -> Caffeine.newBuilder().weakValues().maximumSize(MAX_CLASS_CACHE_PER_LOADER_SIZE).build());
try {
return classLoaderCache.get(className, key -> {
try {
return validatingClassLoader.loadClass(key);
} catch (ClassNotFoundException e) {
throw new ClassLookupException(e);
}
});
} catch (ClassLookupException e) {
// The ClassLookupException only serves to wrap the checked ClassNotFoundException thrown by ClassLoader.loadClass.
throw (ClassNotFoundException) e.getCause();
}
}
/**
* This is a wrapper class to allow the checked ClassNotFoundException thrown by ClassLoader.loadClass to be propagated
* We should always be able to unwrap this exception without risk of ClassCastException since the only code that can throw it is the mapping function passed to the cache
* and it only ever throws this wrapper with a ClassNotFoundException cause.
*/
private static final class ClassLookupException extends RuntimeException {
private ClassLookupException(ClassNotFoundException cause) {
super(cause);
}
}
public static Set<String> toPackageNamesSet(String newDelimitedPackageNames) throws ConfigurationException {
Set<String> packageNames = commaDelimitedStringToSet(newDelimitedPackageNames)
.stream().map(s -> strip(s, ".")).collect(toSet());
@@ -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);
}
@@ -33,6 +33,9 @@ import java.util.ResourceBundle;
*/
public class GlobalLocalizedTextProvider extends AbstractLocalizedTextProvider {
// Pinned to the value implicitly computed for the pre-6.11.0 class shape, see AbstractLocalizedTextProvider.
private static final long serialVersionUID = 7569216885652454296L;
private static final Logger LOG = LogManager.getLogger(GlobalLocalizedTextProvider.class);
public GlobalLocalizedTextProvider() {
@@ -0,0 +1,71 @@
/*
* 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 com.opensymphony.xwork2.util;
import java.util.function.Supplier;
/**
* A thread-safe lazy reference that computes its value on first access using
* double-checked locking. The cached value can be invalidated via {@link #reset()},
* causing the next {@link #get()} call to recompute the value.
*
* @param <T> the type of the lazily computed value
* @since 6.9.0
*/
public class LazyRef<T> implements Supplier<T> {
private final Supplier<T> factory;
private volatile T value;
/**
* Creates a new LazyRef with the given factory supplier.
*
* @param factory the supplier used to compute the value; must not be null
*/
public LazyRef(Supplier<T> factory) {
this.factory = factory;
}
/**
* Returns the cached value, computing it on first access or after a {@link #reset()}.
*
* @return the computed value
*/
@Override
public T get() {
T result = value;
if (result == null) {
synchronized (this) {
result = value;
if (result == null) {
result = factory.get();
value = result;
}
}
}
return result;
}
/**
* Invalidates the cached value so the next {@link #get()} call recomputes it.
*/
public void reset() {
value = null;
}
}
@@ -21,6 +21,7 @@ package com.opensymphony.xwork2.util;
import com.opensymphony.xwork2.ognl.DefaultOgnlCacheFactory;
import com.opensymphony.xwork2.ognl.OgnlCache;
import com.opensymphony.xwork2.ognl.OgnlCacheFactory;
import com.opensymphony.xwork2.ognl.ProxyCacheFactory;
import org.apache.commons.lang3.reflect.ConstructorUtils;
import org.apache.commons.lang3.reflect.FieldUtils;
import org.apache.commons.lang3.reflect.MethodUtils;
@@ -41,7 +42,6 @@ import static java.lang.reflect.Modifier.isPublic;
* <p>
* Various utility methods dealing with proxies
* </p>
*
*/
public class ProxyUtil {
private static final String SPRING_ADVISED_CLASS_NAME = "org.springframework.aop.framework.Advised";
@@ -51,15 +51,56 @@ public class ProxyUtil {
private static final String HIBERNATE_HIBERNATEPROXY_CLASS_NAME = "org.hibernate.proxy.HibernateProxy";
private static final int CACHE_MAX_SIZE = 10000;
private static final int CACHE_INITIAL_CAPACITY = 256;
private static final OgnlCache<Class<?>, Boolean> isProxyCache = new DefaultOgnlCacheFactory<Class<?>, Boolean>(
CACHE_MAX_SIZE, OgnlCacheFactory.CacheType.WTLFU, CACHE_INITIAL_CAPACITY).buildOgnlCache();
private static final OgnlCache<Member, Boolean> isProxyMemberCache = new DefaultOgnlCacheFactory<Member, Boolean>(
CACHE_MAX_SIZE, OgnlCacheFactory.CacheType.WTLFU, CACHE_INITIAL_CAPACITY).buildOgnlCache();
private static final boolean HIBERNATE_AVAILABLE = detectHibernate();
private static boolean detectHibernate() {
try {
Class.forName("org.hibernate.proxy.HibernateProxy");
return true;
} catch (ClassNotFoundException e) {
return false;
}
}
// Holder for the cache factory (set by container)
private static volatile ProxyCacheFactory<?, ?> cacheFactory;
// Lazy-initialized caches with reset support
private static final LazyRef<OgnlCache<Class<?>, Boolean>> isProxyCache =
new LazyRef<>(ProxyUtil::createCache);
private static final LazyRef<OgnlCache<Member, Boolean>> isProxyMemberCache =
new LazyRef<>(ProxyUtil::createCache);
/**
* Sets the cache factory. Called by the container during initialization.
* Resets existing caches so they are recreated with the new factory.
*
* @param factory the cache factory to use for creating proxy caches
* @since 6.9.0
*/
public static void setProxyCacheFactory(ProxyCacheFactory<?, ?> factory) {
cacheFactory = factory;
isProxyCache.reset();
isProxyMemberCache.reset();
}
@SuppressWarnings("unchecked")
private static <K, V> OgnlCache<K, V> createCache() {
if (cacheFactory != null) {
return ((ProxyCacheFactory<K, V>) cacheFactory).buildOgnlCache(
CACHE_MAX_SIZE, CACHE_INITIAL_CAPACITY, 0.75f, cacheFactory.getDefaultCacheType());
}
// Fallback to BASIC if container hasn't initialized yet
return new DefaultOgnlCacheFactory<K, V>(
CACHE_MAX_SIZE, OgnlCacheFactory.CacheType.BASIC, CACHE_INITIAL_CAPACITY).buildOgnlCache();
}
/**
* 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})
@@ -78,24 +119,26 @@ public class ProxyUtil {
/**
* Check whether the given object is a proxy.
*
* @param object the object to check
*/
public static boolean isProxy(Object object) {
if (object == null) return false;
Class<?> clazz = object.getClass();
Boolean flag = isProxyCache.get(clazz);
Boolean flag = isProxyCache.get().get(clazz);
if (flag != null) {
return flag;
}
boolean isProxy = isSpringAopProxy(object) || isHibernateProxy(object);
isProxyCache.put(clazz, isProxy);
isProxyCache.get().put(clazz, isProxy);
return isProxy;
}
/**
* 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
*/
@@ -104,14 +147,14 @@ public class ProxyUtil {
return false;
}
Boolean flag = isProxyMemberCache.get(member);
Boolean flag = isProxyMemberCache.get().get(member);
if (flag != null) {
return flag;
}
boolean isProxyMember = isSpringProxyMember(member) || isHibernateProxyMember(member);
isProxyMemberCache.put(member, isProxyMember);
isProxyMemberCache.get().put(member, isProxyMember);
return isProxyMember;
}
@@ -121,6 +164,7 @@ public class ProxyUtil {
* @param object the object to check
*/
public static boolean isHibernateProxy(Object object) {
if (!HIBERNATE_AVAILABLE) return false;
try {
return object != null && HibernateProxy.class.isAssignableFrom(object.getClass());
} catch (NoClassDefFoundError ignored) {
@@ -134,6 +178,7 @@ public class ProxyUtil {
* @param member the member to check
*/
public static boolean isHibernateProxyMember(Member member) {
if (!HIBERNATE_AVAILABLE) return false;
try {
Class<?> clazz = ClassLoaderUtil.loadClass(HIBERNATE_HIBERNATEPROXY_CLASS_NAME, ProxyUtil.class);
return hasMember(clazz, member);
@@ -147,6 +192,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})
@@ -170,6 +216,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) {
@@ -180,6 +227,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) {
@@ -201,6 +249,7 @@ public class ProxyUtil {
/**
* Obtain the singleton target object behind the given spring proxy, if any.
*
* @param candidate the (potential) spring proxy to check
* @return the singleton target object, or {@code null} in any other case
* (not a spring proxy, not an existing singleton target)
@@ -221,6 +270,7 @@ public class ProxyUtil {
/**
* Check whether the specified class is a CGLIB-generated class.
*
* @param clazz the class to check
*/
private static boolean isCglibProxyClass(Class<?> clazz) {
@@ -229,7 +279,8 @@ public class ProxyUtil {
/**
* Check whether the given class implements an interface with a given class name.
* @param clazz the class to check
*
* @param clazz the class to check
* @param ifaceClassName the interface class name to check
*/
private static boolean implementsInterface(Class<?> clazz, String ifaceClassName) {
@@ -243,7 +294,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) {
@@ -264,6 +316,7 @@ public class ProxyUtil {
* @return the target instance of the given object if it is a Hibernate proxy object, otherwise the given object
*/
public static Object getHibernateProxyTarget(Object object) {
if (!HIBERNATE_AVAILABLE) return object;
try {
return Hibernate.unproxy(object);
} catch (NoClassDefFoundError ignored) {
@@ -36,6 +36,9 @@ import java.util.ResourceBundle;
*/
public class StrutsLocalizedTextProvider extends AbstractLocalizedTextProvider {
// Pinned to the value implicitly computed for the pre-6.11.0 class shape, see AbstractLocalizedTextProvider.
private static final long serialVersionUID = -4377984952850818176L;
private static final Logger LOG = LogManager.getLogger(StrutsLocalizedTextProvider.class);
/**
@@ -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 com.opensymphony.xwork2.util;
import com.opensymphony.xwork2.inject.Inject;
import com.opensymphony.xwork2.ognl.ProxyCacheFactory;
/**
* Bean that wires the ProxyCacheFactory to ProxyUtil during container initialization.
* <p>
* This bean is created by the container and receives the configured ProxyCacheFactory
* via dependency injection, then passes it to the static ProxyUtil class.
*
* @since 6.9.0
*/
public class StrutsProxyCacheFactoryBean {
@Inject
public StrutsProxyCacheFactoryBean(ProxyCacheFactory<?, ?> proxyCacheFactory) {
ProxyUtil.setProxyCacheFactory(proxyCacheFactory);
}
}
@@ -21,6 +21,7 @@ package com.opensymphony.xwork2.util.fs;
import com.opensymphony.xwork2.FileManager;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.apache.struts2.dispatcher.InternalDestroyable;
import java.io.IOException;
import java.io.InputStream;
@@ -33,7 +34,7 @@ import java.util.regex.Pattern;
/**
* Default implementation of {@link FileManager}
*/
public class DefaultFileManager implements FileManager {
public class DefaultFileManager implements FileManager, InternalDestroyable {
private static Logger LOG = LogManager.getLogger(DefaultFileManager.class);
@@ -43,6 +44,22 @@ public class DefaultFileManager implements FileManager {
protected static final Map<String, Revision> files = Collections.synchronizedMap(new HashMap<String, Revision>());
private static final List<URL> lazyMonitoredFilesCache = Collections.synchronizedList(new ArrayList<URL>());
/**
* Clears both the files and lazy monitored files caches to prevent classloader leaks on hot redeploy.
*/
public static void clearCache() {
files.clear();
lazyMonitoredFilesCache.clear();
}
/**
* @since 6.9.0
*/
@Override
public void destroy() {
clearCache();
}
protected boolean reloadingConfigs = false;
public DefaultFileManager() {
@@ -257,9 +257,13 @@ public class ValidationInterceptor extends MethodFilterInterceptor {
}
}
protected void doBeforeInvocation(org.apache.struts2.ActionInvocation invocation) throws Exception {
doBeforeInvocation(ActionInvocation.adapt(invocation));
}
@Override
protected String doIntercept(ActionInvocation invocation) throws Exception {
doBeforeInvocation(invocation);
doBeforeInvocation((org.apache.struts2.ActionInvocation) invocation);
return invocation.invoke();
}
@@ -93,9 +93,11 @@ public interface ActionProxy {
String getMethod();
/**
* Gets status of the method value's initialization.
* Gets status of the method value's initialization. Returns {@code true} when the method was explicitly provided
* (e.g. via URL parameter, wildcard substitution, or action configuration), and {@code false} only when the
* framework defaults to {@code "execute"} because no method was specified anywhere.
*
* @return true if the method returned by getMethod() is not a default initializer value.
* @return true if the method returned by getMethod() is not the default "execute" fallback.
*/
boolean isMethodSpecified();
@@ -100,6 +100,14 @@ public final class StrutsConstants {
/** The default locale for the Struts application */
public static final String STRUTS_LOCALE = "struts.locale";
/**
* When enabled, request-derived locales (from {@code Accept-Language}, used when {@code struts.locale} is
* unset) are restricted to the JVM's available-locale set; unavailable values fall back to the default.
*
* @since 6.11.0
*/
public static final String STRUTS_LOCALE_VALIDATE_REQUEST = "struts.locale.validateRequestLocale";
/** Whether to use a Servlet request parameter workaround necessary for some versions of WebLogic */
public static final String STRUTS_DISPATCHER_PARAMETERSWORKAROUND = "struts.dispatcher.parametersWorkaround";
@@ -288,6 +296,22 @@ public final class StrutsConstants {
*/
public static final String STRUTS_OGNL_BEANINFO_CACHE_FACTORY = "struts.ognl.beanInfoCacheFactory";
/**
* Specifies the type of cache to use for the localized-text provider caches. Valid values defined in
* {@link com.opensymphony.xwork2.ognl.OgnlCacheFactory.CacheType}.
*
* @since 6.11.0
*/
public static final String STRUTS_I18N_CACHE_TYPE = "struts.i18n.cacheType";
/**
* Specifies the maximum size of each localized-text provider cache. Configure based on the cache type
* chosen and application-specific needs.
*
* @since 6.11.0
*/
public static final String STRUTS_I18N_CACHE_MAXSIZE = "struts.i18n.cacheMaxSize";
/**
* Specifies the type of cache to use for BeanInfo objects.
* @since 6.4.0
@@ -411,6 +435,7 @@ public final class StrutsConstants {
public static final String STRUTS_CONVERTER_ANNOTATION_PROCESSOR = "struts.converter.annotation.processor";
public static final String STRUTS_CONVERTER_CREATOR = "struts.converter.creator";
public static final String STRUTS_CONVERTER_HOLDER = "struts.converter.holder";
public static final String STRUTS_CONVERTER_USER_PROPERTIES_PROVIDER = "struts.converter.userPropertiesProvider";
public static final String STRUTS_EXPRESSION_PARSER = "struts.expression.parser";
@@ -516,4 +541,25 @@ public final class StrutsConstants {
*/
public static final String STRUTS_CSP_NONCE_READER = "struts.csp.nonce.reader";
public static final String STRUTS_CSP_NONCE_SOURCE = "struts.csp.nonce.source";
/**
* See {@link org.apache.struts2.action.CspReportAction}
*
* @since 6.11.0
*/
public static final String STRUTS_CSP_REPORT_MAX_SIZE = "struts.csp.report.maxSize";
/**
* Specifies the type of cache to use for proxy detection in ProxyUtil.
* Valid values defined in {@link com.opensymphony.xwork2.ognl.OgnlCacheFactory.CacheType}.
* Default is 'basic' (no Caffeine dependency required).
* @since 6.8.0
*/
public static final String STRUTS_PROXY_CACHE_TYPE = "struts.proxy.cacheType";
/**
* Specifies the maximum cache size for proxy detection caches in ProxyUtil.
* @since 6.8.0
*/
public static final String STRUTS_PROXY_CACHE_MAXSIZE = "struts.proxy.cacheMaxSize";
}
@@ -19,11 +19,16 @@
package org.apache.struts2.action;
import com.opensymphony.xwork2.ActionSupport;
import com.opensymphony.xwork2.inject.Inject;
import org.apache.commons.lang3.StringUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.apache.struts2.StrutsConstants;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.Reader;
import static org.apache.struts2.interceptor.csp.CspSettings.CSP_REPORT_TYPE;
@@ -51,7 +56,58 @@ import static org.apache.struts2.interceptor.csp.CspSettings.CSP_REPORT_TYPE;
* @see DefaultCspReportAction
*/
public abstract class CspReportAction extends ActionSupport implements ServletRequestAware, ServletResponseAware {
private static final Logger LOG = LogManager.getLogger(CspReportAction.class);
/**
* Default upper bound, in characters, on the report body accepted by {@link #withServletRequest}.
* CSP violation reports are small JSON documents; anything larger is not treated as a report.
*/
public static final int DEFAULT_MAX_REPORT_SIZE = 8192;
/**
* Largest value accepted for {@code struts.csp.report.maxSize}. A configured value above this is
* ignored, so that a mistyped setting cannot size a per-request buffer large enough to exhaust
* memory.
*/
private static final int MAX_REPORT_SIZE_LIMIT = 1024 * 1024;
private HttpServletRequest request;
private int maxReportSize = DEFAULT_MAX_REPORT_SIZE;
/**
* Sets the upper bound, in characters, on an accepted report body. A body exceeding this size is
* discarded and not passed to {@link #processReport(String)}.
* <p>
* The value is injected from {@code struts.csp.report.maxSize} when the action is built, which is
* before the interceptor stack runs. It is deliberately not an action property: the report body is
* read by {@link #withServletRequest(HttpServletRequest)}, which the {@code servletConfig}
* interceptor invokes ahead of {@code staticParams} and {@code params}, so a value applied by
* either of those would arrive too late to have any effect.
*
* @param maxReportSize maximum accepted report size in characters
* @since 6.11.0
*/
@Inject(value = StrutsConstants.STRUTS_CSP_REPORT_MAX_SIZE, required = false)
public void setMaxReportSize(String maxReportSize) {
if (StringUtils.isBlank(maxReportSize)) {
return;
}
int size;
try {
size = Integer.parseInt(maxReportSize.trim());
} catch (NumberFormatException e) {
LOG.warn("Ignoring non-numeric {} value: {}, keeping {}",
StrutsConstants.STRUTS_CSP_REPORT_MAX_SIZE, maxReportSize, this.maxReportSize);
return;
}
if (size < 1 || size > MAX_REPORT_SIZE_LIMIT) {
LOG.warn("Ignoring out-of-range {} value: {}, expected 1..{}, keeping {}",
StrutsConstants.STRUTS_CSP_REPORT_MAX_SIZE, size, MAX_REPORT_SIZE_LIMIT, this.maxReportSize);
return;
}
this.maxReportSize = size;
}
@Override
public void withServletRequest(HttpServletRequest request) {
@@ -60,13 +116,36 @@ public abstract class CspReportAction extends ActionSupport implements ServletRe
}
try {
BufferedReader reader = request.getReader();
String cspReport = reader.readLine();
String cspReport = readReport(request.getReader());
if (cspReport == null) {
LOG.warn("Discarding CSP report larger than the configured limit of {} characters", maxReportSize);
return;
}
processReport(cspReport);
} catch (IOException ignored) {
}
}
/**
* Reads at most {@link #maxReportSize} characters from the report body.
*
* @param reader reader over the report body
* @return the report body, or {@code null} if it exceeds the limit
* @throws IOException if the body cannot be read
*/
private String readReport(Reader reader) throws IOException {
char[] buffer = new char[maxReportSize];
int total = 0;
int read;
while (total < buffer.length && (read = reader.read(buffer, total, buffer.length - total)) != -1) {
total += read;
}
if (total == buffer.length && reader.read() != -1) {
return null;
}
return new String(buffer, 0, total);
}
private boolean isCspReportRequest(HttpServletRequest request) {
if (!"POST".equals(request.getMethod()) || request.getContentLength() <= 0){
return false;
@@ -68,6 +68,13 @@ public class Component {
*/
protected static ConcurrentMap<Class<?>, Collection<String>> standardAttributesMap = new ConcurrentHashMap<>();
/**
* Clears the cached standard attributes map to prevent classloader leaks on hot redeploy.
*/
public static void clearStandardAttributesMap() {
standardAttributesMap.clear();
}
protected boolean devMode = false;
protected boolean escapeHtmlBody = false;
protected ValueStack stack;
@@ -30,7 +30,44 @@ import org.apache.logging.log4j.Logger;
import java.util.Properties;
/**
* TODO lukaszlenart: write a JavaDoc
* Base implementation of {@link BeanSelectionProvider} that provides bean aliasing functionality.
* <p>
* This class provides the {@link #alias(Class, String, ContainerBuilder, Properties, Scope)} method
* which is used to select and register bean implementations based on configuration properties.
* </p>
*
* <h2>Bean Selection Process</h2>
* <p>
* The {@code alias} method selects a bean implementation using the following process:
* </p>
* <ol>
* <li>Read the property value for the given key from the configuration properties</li>
* <li>If no property is set, use {@value #DEFAULT_BEAN_NAME} as the default bean name</li>
* <li>Check if a bean with that name already exists in the container:
* <ul>
* <li>If found, alias it to {@link Container#DEFAULT_NAME} making it the default</li>
* <li>If not found, try to load the property value as a fully qualified class name</li>
* </ul>
* </li>
* <li>If class loading succeeds, register the class as a factory for the interface type</li>
* <li>If class loading fails and the name is not the default, create a delegate factory
* that will resolve the bean through {@link ObjectFactory} at runtime. This allows
* Spring bean names to be used in configuration.</li>
* </ol>
*
* <h2>Usage Example</h2>
* <pre>
* // In struts.properties or struts.xml:
* // struts.objectFactory = spring
* // struts.converter.collection = myCustomCollectionConverter
*
* // In a subclass:
* alias(ObjectFactory.class, StrutsConstants.STRUTS_OBJECTFACTORY, builder, props);
* alias(CollectionConverter.class, StrutsConstants.STRUTS_CONVERTER_COLLECTION, builder, props);
* </pre>
*
* @see BeanSelectionProvider
* @see StrutsBeanSelectionProvider
*/
public abstract class AbstractBeanSelectionProvider implements BeanSelectionProvider {
@@ -73,7 +110,7 @@ public abstract class AbstractBeanSelectionProvider implements BeanSelectionProv
// Perhaps a spring bean id, so we'll delegate to the object factory at runtime
LOG.trace("Choosing bean ({}) for ({}) to be loaded from the ObjectFactory", foundName, type.getName());
if (DEFAULT_BEAN_NAME.equals(foundName)) {
// Probably an optional bean, will ignore
LOG.trace("No bean registered for type ({}) with default name '{}', skipping as optional", type.getName(), DEFAULT_BEAN_NAME);
} else {
if (ObjectFactory.class != type) {
builder.factory(type, new ObjectFactoryDelegateFactory(foundName, type), scope);
@@ -103,7 +140,7 @@ public abstract class AbstractBeanSelectionProvider implements BeanSelectionProv
try {
return objFactory.buildBean(name, null, true);
} catch (ClassNotFoundException ex) {
throw new ConfigurationException("Unable to load bean "+type.getName()+" ("+name+")");
throw new ConfigurationException(String.format("Unable to load bean %s (name = %s)", type.getName(), name));
}
}
@@ -65,6 +65,7 @@ import ognl.MethodAccessor;
import org.apache.struts2.StrutsConstants;
import org.apache.struts2.components.UrlRenderer;
import org.apache.struts2.components.date.DateFormatter;
import org.apache.struts2.conversion.UserConversionPropertiesProvider;
import org.apache.struts2.dispatcher.DispatcherErrorHandler;
import org.apache.struts2.dispatcher.StaticContentLoader;
import org.apache.struts2.dispatcher.mapper.ActionMapper;
@@ -88,7 +89,7 @@ import org.apache.struts2.views.util.UrlHelper;
*
* <p>
* The following is a list of the allowed extension points:
*
* <p>
* <!-- START SNIPPET: extensionPoints -->
* <table border="1" summary="">
* <tr>
@@ -353,7 +354,7 @@ import org.apache.struts2.views.util.UrlHelper;
* <td>Provides access to resource bundles used to localise messages (since 2.5.11)</td>
* </tr>
* </table>
*
* <p>
* <!-- END SNIPPET: extensionPoints -->
*
* <p>
@@ -405,6 +406,7 @@ public class StrutsBeanSelectionProvider extends AbstractBeanSelectionProvider {
alias(ConversionAnnotationProcessor.class, StrutsConstants.STRUTS_CONVERTER_ANNOTATION_PROCESSOR, builder, props);
alias(TypeConverterCreator.class, StrutsConstants.STRUTS_CONVERTER_CREATOR, builder, props);
alias(TypeConverterHolder.class, StrutsConstants.STRUTS_CONVERTER_HOLDER, builder, props);
alias(UserConversionPropertiesProvider.class, StrutsConstants.STRUTS_CONVERTER_USER_PROPERTIES_PROVIDER, builder, props);
alias(TextProvider.class, StrutsConstants.STRUTS_TEXT_PROVIDER, builder, props, Scope.PROTOTYPE);
alias(TextProviderFactory.class, StrutsConstants.STRUTS_TEXT_PROVIDER_FACTORY, builder, props, Scope.PROTOTYPE);
@@ -35,7 +35,7 @@ import java.util.Iterator;
import java.util.Map;
import java.util.Properties;
public class StrutsConversionPropertiesProcessor implements ConversionPropertiesProcessor, EarlyInitializable {
public class StrutsConversionPropertiesProcessor implements ConversionPropertiesProcessor, EarlyInitializable, UserConversionPropertiesProvider {
private static final Logger LOG = LogManager.getLogger(StrutsConversionPropertiesProcessor.class);
@@ -58,8 +58,27 @@ public class StrutsConversionPropertiesProcessor implements ConversionProperties
@Override
public void init() {
LOG.debug("Processing default conversion properties files");
// Early phase: Only process framework defaults (class names only)
// User properties are processed later in initUserConversions() when
// SpringObjectFactory is available for bean name resolution (WW-4291)
LOG.debug("Processing default conversion properties files (early phase)");
processRequired(STRUTS_DEFAULT_CONVERSION_PROPERTIES);
}
/**
* Process user conversion properties. Called during late initialization
* when SpringObjectFactory is available for bean name resolution.
* <p>
* This allows users to reference Spring bean names in struts-conversion.properties
* instead of only fully qualified class names.
* </p>
*
* @see <a href="https://issues.apache.org/jira/browse/WW-4291">WW-4291</a>
* @since 7.2.0
*/
@Override
public void initUserConversions() {
LOG.debug("Processing user conversion properties files (late phase)");
process(STRUTS_CONVERSION_PROPERTIES);
process(XWORK_CONVERSION_PROPERTIES);
}
@@ -78,7 +97,7 @@ public class StrutsConversionPropertiesProcessor implements ConversionProperties
while (resources.hasNext()) {
if (XWORK_CONVERSION_PROPERTIES.equals(propsName)) {
LOG.warn("Instead of using deprecated {} please use the new file name {}",
XWORK_CONVERSION_PROPERTIES, STRUTS_CONVERSION_PROPERTIES);
XWORK_CONVERSION_PROPERTIES, STRUTS_CONVERSION_PROPERTIES);
}
URL url = resources.next();
Properties props = new Properties();
@@ -86,8 +105,7 @@ public class StrutsConversionPropertiesProcessor implements ConversionProperties
LOG.debug("Processing conversion file [{}]", propsName);
for (Object o : props.entrySet()) {
Map.Entry entry = (Map.Entry) o;
for (Map.Entry<Object, Object> entry : props.entrySet()) {
String key = (String) entry.getKey();
try {
@@ -0,0 +1,55 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.struts2.conversion;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import com.opensymphony.xwork2.inject.Initializable;
import com.opensymphony.xwork2.inject.Inject;
/**
* Late initialization processor for user conversion properties.
* <p>
* Processes struts-conversion.properties and xwork-conversion.properties
* after the full container is built, allowing Spring bean name resolution.
* This enables users to reference Spring bean names instead of only fully
* qualified class names in their conversion property files.
* </p>
*
* @see <a href="https://issues.apache.org/jira/browse/WW-4291">WW-4291</a>
* @see UserConversionPropertiesProvider
* @since 6.9.0
*/
public class UserConversionPropertiesProcessor implements Initializable {
private static final Logger LOG = LogManager.getLogger(UserConversionPropertiesProcessor.class);
private UserConversionPropertiesProvider provider;
@Inject
public void setUserConversionPropertiesProvider(UserConversionPropertiesProvider provider) {
this.provider = provider;
}
@Override
public void init() {
LOG.debug("Initializing user conversion properties via late initialization");
provider.initUserConversions();
}
}
@@ -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.conversion;
/**
* Interface for processors that support late initialization of user conversion properties.
* <p>
* Implementations provide user conversion properties processing after the full container
* is built, allowing Spring bean name resolution for type converters.
* </p>
*
* @see <a href="https://issues.apache.org/jira/browse/WW-4291">WW-4291</a>
* @since 6.9.0
*/
public interface UserConversionPropertiesProvider {
/**
* Process user conversion properties (struts-conversion.properties, xwork-conversion.properties).
* Called during late initialization when SpringObjectFactory is available.
*/
void initUserConversions();
}
@@ -0,0 +1,36 @@
/*
* 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. Wrapper is needed because {@code Component}
* requires constructor arguments that prevent direct container instantiation.
*
* @since 6.9.0
*/
public class ComponentCacheDestroyable implements InternalDestroyable {
@Override
public void destroy() {
Component.clearStandardAttributesMap();
}
}
@@ -21,27 +21,70 @@ package org.apache.struts2.dispatcher;
import com.opensymphony.xwork2.inject.Container;
/**
* 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 com.opensymphony.xwork2.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 a volatile 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 volatile long generation = 0;
public static void store(Container newInstance) {
instance.set(newInstance);
instance.set(new CachedContainer(newInstance, generation));
}
public static Container get() {
return instance.get();
CachedContainer cached = instance.get();
if (cached == null) {
return null;
}
if (cached.generation != generation) {
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++;
instance.remove();
}
private static class CachedContainer {
final Container container;
final long generation;
CachedContainer(Container container, long generation) {
this.container = container;
this.generation = 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 javax.servlet.ServletContext;
/**
* Extension of {@link InternalDestroyable} for components that require
* {@link ServletContext} during cleanup (e.g. clearing servlet-scoped caches).
*
* <p>During {@link Dispatcher#cleanup()}, the discovery loop checks each
* {@code InternalDestroyable} bean: if it implements this subinterface,
* {@link #destroy(ServletContext)} is called instead of {@link #destroy()}.</p>
*
* @since 6.9.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
}
}
@@ -152,6 +152,11 @@ public class Dispatcher {
*/
private String defaultLocale;
/**
* Store state of {@link StrutsConstants#STRUTS_LOCALE_VALIDATE_REQUEST} setting.
*/
private boolean validateRequestLocale = false;
/**
* Store state of StrutsConstants.STRUTS_MULTIPART_SAVEDIR setting.
*/
@@ -311,6 +316,18 @@ public class Dispatcher {
defaultLocale = val;
}
/**
* Modify state of {@link StrutsConstants#STRUTS_LOCALE_VALIDATE_REQUEST} setting.
*
* @param val New setting
*
* @since 6.11.0
*/
@Inject(value = StrutsConstants.STRUTS_LOCALE_VALIDATE_REQUEST, required = false)
public void setValidateRequestLocale(String val) {
validateRequestLocale = Boolean.parseBoolean(val);
}
/**
* Modify state of StrutsConstants.STRUTS_I18N_ENCODING setting.
*
@@ -451,7 +468,37 @@ public class Dispatcher {
* Releases all instances bound to this dispatcher instance.
*/
public void cleanup() {
// clean up ObjectFactory
destroyObjectFactory();
// clean up Dispatcher itself for this thread
instance.remove();
servletContext.setAttribute(StrutsStatics.SERVLET_DISPATCHER, null);
destroyDispatcherListeners();
destroyInterceptors();
destroyInternalBeans();
// WW-5537: Invalidate all threads' cached Container references to prevent
// classloader leaks from idle pool threads retaining stale references after undeploy.
ContainerHolder.invalidateAll();
//cleanup action context
ActionContext.clear();
// clean up configuration
configurationManager.destroyConfiguration();
configurationManager = null;
}
/**
* Destroys the {@link ObjectFactory} if it implements {@link ObjectFactoryDestroyable}.
* Called at the beginning of {@link #cleanup()}.
*
* @since 6.9.0
*/
protected void destroyObjectFactory() {
if (objectFactory == null) {
LOG.warn("Object Factory is null, something is seriously wrong, no clean up will be performed");
}
@@ -459,23 +506,36 @@ public class Dispatcher {
try {
((ObjectFactoryDestroyable) objectFactory).destroy();
} catch (Exception e) {
// catch any exception that may occur during destroy() and log it
LOG.error("Exception occurred while destroying ObjectFactory [{}]", objectFactory.toString(), e);
}
}
}
// clean up Dispatcher itself for this thread
instance.remove();
servletContext.setAttribute(StrutsStatics.SERVLET_DISPATCHER, null);
// clean up DispatcherListeners
/**
* Notifies all registered {@link DispatcherListener}s that this dispatcher
* is being destroyed, then clears the listener list.
*
* @since 6.9.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. Listeners must be re-registered
// if a new Dispatcher is created (e.g. on redeploy).
dispatcherListeners.clear();
}
}
// clean up all interceptors by calling their destroy() method
/**
* Destroys all interceptors registered in the current configuration.
* Called during {@link #cleanup()} before {@link #destroyInternalBeans()}.
*
* @since 6.9.0
*/
protected void destroyInterceptors() {
Set<Interceptor> interceptors = new HashSet<>();
Collection<PackageConfig> packageConfigs = configurationManager.getConfiguration().getPackageConfigs().values();
for (PackageConfig packageConfig : packageConfigs) {
@@ -490,16 +550,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 javax.servlet.ServletContext} via
* {@link ContextAwareDestroyable#destroy(javax.servlet.ServletContext)}.</p>
*
* @since 6.9.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) {
((ContextAwareDestroyable) destroyable).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 {
@@ -885,7 +967,7 @@ public class Dispatcher {
locale = LocaleUtils.toLocale(defaultLocale);
} catch (IllegalArgumentException e) {
try {
locale = request.getLocale();
locale = resolveRequestLocale(request);
LOG.warn(new ParameterizedMessage("Cannot convert 'struts.locale' = [{}] to proper locale, defaulting to request locale [{}]",
defaultLocale, locale), e);
} catch (RuntimeException rex) {
@@ -896,7 +978,7 @@ public class Dispatcher {
}
} else {
try {
locale = request.getLocale();
locale = resolveRequestLocale(request);
} catch (RuntimeException rex) {
LOG.warn("Cannot get locale from HTTP Request, falling back to system default locale", rex);
locale = Locale.getDefault();
@@ -905,6 +987,33 @@ public class Dispatcher {
return locale;
}
/**
* Resolves the request locale. When {@code struts.locale.validateRequestLocale} is enabled and the
* request locale is not part of the JVM's available-locale set, falls back to the configured
* {@code struts.locale} when set and parseable, otherwise the JVM default. When disabled (default),
* returns the request locale unchanged.
*
* @param request the current request
* @return the locale to use for this request
*
* @since 6.11.0
*/
protected Locale resolveRequestLocale(HttpServletRequest request) {
Locale locale = request.getLocale();
if (!validateRequestLocale || LocaleUtils.isAvailableLocale(locale)) {
return locale;
}
if (defaultLocale != null) {
try {
return LocaleUtils.toLocale(defaultLocale);
} catch (IllegalArgumentException e) {
LOG.debug("Configured 'struts.locale' = [{}] is not parseable; falling back to system default", defaultLocale);
}
}
LOG.debug("Request locale [{}] is not available; falling back to system default locale", locale);
return Locale.getDefault();
}
/**
* Return the path to save uploaded files to (this is configurable).
*
@@ -0,0 +1,36 @@
/*
* 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 com.opensymphony.xwork2.inject.util.FinalizableReferenceQueue;
/**
* Adapter that exposes {@link FinalizableReferenceQueue#stopAndClear()} as an
* {@link InternalDestroyable} bean, since {@code FinalizableReferenceQueue}
* has a private constructor and cannot be directly registered in the container.
*
* @since 6.9.0
*/
public class FinalizableReferenceQueueDestroyable implements InternalDestroyable {
@Override
public void destroy() {
FinalizableReferenceQueue.stopAndClear();
}
}
@@ -0,0 +1,57 @@
/*
* 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 javax.servlet.ServletContext;
/**
* WW-5537: Clears FreeMarker's template and class introspection caches
* stored in {@link ServletContext} during application undeploy, preventing
* classloader leaks.
*
* @since 6.9.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) {
Configuration cfg = (Configuration) fmConfig;
cfg.clearTemplateCache();
cfg.clearEncodingMap();
if (cfg.getObjectWrapper() instanceof BeansWrapper) {
((BeansWrapper) cfg.getObjectWrapper()).clearClassIntrospectionCache();
}
servletContext.removeAttribute(FreemarkerManager.CONFIG_SERVLET_CONTEXT_KEY);
LOG.debug("FreeMarker configuration cleaned up");
}
}
}
@@ -0,0 +1,50 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.struts2.dispatcher;
/**
* Internal framework interface for components that hold static state
* (caches, daemon threads, etc.) requiring cleanup during application
* undeploy to prevent classloader leaks.
*
* <p>Implementations are registered as named beans in {@code struts-beans.xml}
* (or plugin descriptors) with type {@code InternalDestroyable}. During
* {@link Dispatcher#cleanup()}, all registered implementations are discovered
* via {@code Container.getInstanceNames(InternalDestroyable.class)} and
* invoked automatically.</p>
*
* <p>The order in which implementations are invoked is not guaranteed.
* Implementations must not depend on other {@code InternalDestroyable}
* beans having been (or not yet been) destroyed. Ordering can be
* influenced via the {@code order} attribute in bean registration.</p>
*
* <p>This is not part of the public user API. For user/plugin lifecycle
* callbacks, use {@link DispatcherListener} instead.</p>
*
* @since 6.9.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 com.opensymphony.xwork2.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 6.9.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,37 @@
/*
* 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. Separated from the interceptor itself because the
* locks map is static and must be cleared regardless of whether the interceptor
* is configured in any package.
*
* @since 6.9.0
*/
public class ScopeInterceptorCacheDestroyable implements InternalDestroyable {
@Override
public void destroy() {
ScopeInterceptor.clearLocks();
}
}
@@ -33,7 +33,11 @@ import java.util.StringTokenizer;
/**
* Extended version of {@link RestfulActionMapper}, see documentation for more details
* <a href="https://struts.apache.org/core-developers/restful-action-mapper.html">Restful2ActionMapper</a>
*
* @deprecated since 6.12.0, this legacy mapper predates the Struts REST plugin, which is the maintained
* way to build REST-style applications with Struts. Scheduled for removal in the next major release.
*/
@Deprecated
public class Restful2ActionMapper extends DefaultActionMapper {
private static final Logger LOG = LogManager.getLogger(Restful2ActionMapper.class);
@@ -23,29 +23,55 @@ import com.opensymphony.xwork2.inject.Inject;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.apache.struts2.RequestUtils;
import org.apache.struts2.StrutsConstants;
import org.apache.struts2.url.UrlDecoder;
import javax.servlet.http.HttpServletRequest;
import java.util.HashMap;
import java.util.Map;
import java.util.StringTokenizer;
import java.util.regex.Pattern;
/**
* Simple Restfull Action Mapper to support REST application
* See docs for more information
* <a href="https://struts.apache.org/core-developers/restful-action-mapper.html">RestfulActionMapper</a>
*
* @deprecated since 6.12.0, this legacy mapper predates the Struts REST plugin, which is the maintained
* way to build REST-style applications with Struts. Scheduled for removal in the next major release.
*/
@Deprecated
public class RestfulActionMapper implements ActionMapper {
protected static final Logger LOG = LogManager.getLogger(RestfulActionMapper.class);
private UrlDecoder decoder;
/**
* Matches action names allowed in the request URI, aligned with {@link DefaultActionMapper}.
*/
private Pattern allowedActionNames = Pattern.compile("[a-zA-Z0-9._!/\\-]*");
/**
* Action name used when the name extracted from the URI is not allowed, aligned with {@link DefaultActionMapper}.
*/
private String defaultActionName = "index";
@Inject
public void setDecoder(UrlDecoder decoder) {
this.decoder = decoder;
}
@Inject(value = StrutsConstants.STRUTS_ALLOWED_ACTION_NAMES, required = false)
public void setAllowedActionNames(String allowedActionNames) {
this.allowedActionNames = Pattern.compile(allowedActionNames);
}
@Inject(value = StrutsConstants.STRUTS_DEFAULT_ACTION_NAME, required = false)
public void setDefaultActionName(String defaultActionName) {
this.defaultActionName = defaultActionName;
}
/* (non-Javadoc)
* @see org.apache.struts2.dispatcher.mapper.ActionMapper#getMapping(javax.servlet.http.HttpServletRequest)
*/
@@ -57,7 +83,7 @@ public class RestfulActionMapper implements ActionMapper {
return null;
}
String actionName = uri.substring(1, nextSlash);
String actionName = cleanupActionName(uri.substring(1, nextSlash));
Map<String, Object> parameters = new HashMap<>();
try {
StringTokenizer st = new StringTokenizer(uri.substring(nextSlash), "/");
@@ -96,6 +122,22 @@ public class RestfulActionMapper implements ActionMapper {
return new ActionMapping(actionName, null, null, null);
}
/**
* Checks action name against the allowed pattern; if it does not match, returns the default action name.
* Mirrors {@link DefaultActionMapper#cleanupActionName(String)}.
*
* @param rawActionName action name extracted from the URI
* @return safe action name
*/
protected String cleanupActionName(final String rawActionName) {
if (allowedActionNames.matcher(rawActionName).matches()) {
return rawActionName;
} else {
LOG.warn("{} did not match allowed action names {} - default action {} will be used!", rawActionName, allowedActionNames, defaultActionName);
return defaultActionName;
}
}
/* (non-Javadoc)
* @see org.apache.struts2.dispatcher.mapper.ActionMapper#getUriFromActionMapping(org.apache.struts2.dispatcher.mapper.ActionMapping)
*/
@@ -103,12 +103,11 @@ public class JakartaMultiPartRequest extends AbstractMultiPartRequest {
protected void processUpload(HttpServletRequest request, String saveDir) throws FileUploadException, UnsupportedEncodingException {
if (ServletFileUpload.isMultipartContent(request)) {
for (FileItem item : parseRequest(request, saveDir)) {
// Track all FileItem instances for comprehensive cleanup - addAll in case exception in for loop
allFileItems.addAll(parseRequest(request, saveDir));
for (FileItem item : allFileItems) {
LOG.debug("Found file item: [{}]", normalizeSpace(item.getFieldName()));
// Track all FileItem instances for comprehensive cleanup
allFileItems.add(item);
if (item.isFormField()) {
processNormalFormField(item, request.getCharacterEncoding());
} else {
@@ -374,10 +373,14 @@ public class JakartaMultiPartRequest extends AbstractMultiPartRequest {
if (item instanceof DiskFileItem) {
DiskFileItem diskItem = (DiskFileItem) item;
File storeLocation = diskItem.getStoreLocation();
if (storeLocation != null && storeLocation.exists()) {
LOG.debug("Deleting temporary file: [{}]", storeLocation.getName());
if (!storeLocation.delete()) {
LOG.warn("Unable to delete temporary file: [{}]", storeLocation.getName());
if (storeLocation != null) {
if(storeLocation.isFile()) {
LOG.debug("Deleting file: {}", storeLocation.getName());
if (!storeLocation.delete()) {
LOG.warn("There was a problem attempting to delete file: {}", storeLocation.getName());
}
} else {
LOG.debug("File: {} already deleted", storeLocation.getName());
}
}
}
@@ -76,10 +76,22 @@ public class JakartaStreamMultiPartRequest extends AbstractMultiPartRequest {
LOG.debug("Performing File Upload temporary storage cleanup.");
for (List<FileInfo> fileInfoList : fileInfos.values()) {
for (FileInfo fileInfo : fileInfoList) {
File file = fileInfo.getFile();
LOG.debug("Deleting file '{}'.", file.getName());
if (!file.delete()) {
LOG.warn("There was a problem attempting to delete file [{}].", file.getName());
try {
// catch any exceptions during cleanup to ensure all files are deleted.
File file = fileInfo.getFile();
if(file != null) {
if(file.isFile()) {
LOG.debug("Deleting file: {}", file.getName());
if (!file.delete()) {
LOG.warn("There was a problem attempting to delete file: {}",
file.getName());
}
} else {
LOG.debug("File: {} already deleted", file.getName());
}
}
} catch (Exception e) {
LOG.warn("Error during cleanup of file item: [{}]", normalizeSpace(fileInfo.getOriginalName()), e);
}
}
}
@@ -65,7 +65,7 @@ public class I18nInterceptor extends AbstractInterceptor {
private Set<Locale> supportedLocale = Collections.emptySet();
protected enum Storage { COOKIE, SESSION, REQUEST, ACCEPT_LANGUAGE }
protected enum Storage {COOKIE, SESSION, REQUEST, ACCEPT_LANGUAGE}
public void setParameterName(String parameterName) {
this.parameterName = parameterName;
@@ -103,10 +103,14 @@ public class I18nInterceptor extends AbstractInterceptor {
*/
public void setSupportedLocale(String supportedLocale) {
this.supportedLocale = TextParseUtil
.commaDelimitedStringToSet(supportedLocale)
.stream()
.map(Locale::new)
.collect(Collectors.toSet());
.commaDelimitedStringToSet(supportedLocale)
.stream()
.map(Locale::new)
.collect(Collectors.toSet());
}
protected boolean isLocaleSupported(Locale locale) {
return supportedLocale.isEmpty() || supportedLocale.contains(locale);
}
@Inject
@@ -222,8 +226,11 @@ public class I18nInterceptor extends AbstractInterceptor {
*/
protected interface LocaleHandler {
Locale find();
Locale read(ActionInvocation invocation);
Locale store(ActionInvocation invocation, Locale locale);
boolean shouldStore();
}
@@ -241,7 +248,10 @@ public class I18nInterceptor extends AbstractInterceptor {
Parameter requestedLocale = findLocaleParameter(actionInvocation, requestOnlyParameterName);
if (requestedLocale.isDefined()) {
return getLocaleFromParam(requestedLocale.getValue());
Locale locale = getLocaleFromParam(requestedLocale.getValue());
if (locale != null && isLocaleSupported(locale)) {
return locale;
}
}
return null;
@@ -278,6 +288,11 @@ public class I18nInterceptor extends AbstractInterceptor {
@Override
@SuppressWarnings("rawtypes")
public Locale find() {
Locale requestOnlyLocale = super.find();
if (requestOnlyLocale != null) {
return requestOnlyLocale;
}
if (!supportedLocale.isEmpty()) {
Enumeration locales = actionInvocation.getInvocationContext().getServletRequest().getLocales();
while (locales.hasMoreElements()) {
@@ -287,7 +302,7 @@ public class I18nInterceptor extends AbstractInterceptor {
}
}
}
return super.find();
return null;
}
}
@@ -300,18 +315,18 @@ public class I18nInterceptor extends AbstractInterceptor {
@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());
Locale locale = getLocaleFromParam(requestedLocale.getValue());
if (locale != null && isLocaleSupported(locale)) {
return locale;
}
}
Locale requestOnlyLocale = super.find();
if (requestOnlyLocale != null) {
shouldStore = false;
return requestOnlyLocale;
}
return null;
@@ -344,7 +359,12 @@ public class I18nInterceptor extends AbstractInterceptor {
Object sessionLocale = invocation.getInvocationContext().getSession().get(attributeName);
if (sessionLocale instanceof Locale) {
locale = (Locale) sessionLocale;
LOG.debug("Applied session locale: {}", locale);
if (!isLocaleSupported(locale)) {
LOG.debug("Stored session locale {} is not supported, discarding", locale);
locale = null;
} else {
LOG.debug("Applied session locale: {}", locale);
}
}
}
}
@@ -368,17 +388,18 @@ public class I18nInterceptor extends AbstractInterceptor {
@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());
Locale locale = getLocaleFromParam(requestedLocale.getValue());
if (locale != null && isLocaleSupported(locale)) {
return locale;
}
}
Locale requestOnlyLocale = super.find();
if (requestOnlyLocale != null) {
shouldStore = false;
return requestOnlyLocale;
}
return null;
@@ -404,6 +425,10 @@ public class I18nInterceptor extends AbstractInterceptor {
for (Cookie cookie : cookies) {
if (attributeName.equals(cookie.getName())) {
locale = getLocaleFromParam(cookie.getValue());
if (locale != null && !isLocaleSupported(locale)) {
LOG.debug("Stored cookie locale {} is not supported, discarding", locale);
locale = null;
}
}
}
}
@@ -232,7 +232,21 @@ public class ScopeInterceptor extends AbstractInterceptor implements PreResultLi
return o;
}
private static Map<Object, Object> locks = new IdentityHashMap<>();
private static final Map<Object, Object> locks = new IdentityHashMap<>();
/**
* Clears the locks map to prevent classloader leaks on hot redeploy.
*/
public static void clearLocks() {
synchronized (locks) {
locks.clear();
}
}
@Override
public void destroy() {
clearLocks();
}
static void lock(Object o, ActionInvocation invocation) throws Exception {
synchronized (o) {
@@ -135,7 +135,7 @@ public class TokenInterceptor extends MethodFilterInterceptor {
@Override
protected String doIntercept(ActionInvocation invocation) throws Exception {
LOG.debug("Intercepting invocation to check for valid transaction token.");
return handleToken(invocation);
return handleToken((org.apache.struts2.ActionInvocation) invocation);
}
protected String handleToken(ActionInvocation invocation) throws Exception {
@@ -144,22 +144,19 @@ public class TokenInterceptor extends MethodFilterInterceptor {
HttpSession session = ServletActionContext.getRequest().getSession(true);
synchronized (session.getId().intern()) {
if (!TokenHelper.validToken()) {
return handleInvalidToken(invocation);
return handleInvalidToken((org.apache.struts2.ActionInvocation) invocation);
}
}
return handleValidToken(invocation);
return handleValidToken((org.apache.struts2.ActionInvocation) invocation);
}
protected String handleToken(org.apache.struts2.ActionInvocation invocation) throws Exception {
return handleToken(ActionInvocation.adapt(invocation));
}
/**
* Determines what to do if an invalid token is provided. If the action implements {@link ValidationAware}
*
* @param invocation the action invocation where the invalid token failed
* @return the return code to indicate should be processed
* @throws Exception when any unexpected error occurs.
*/
protected String handleInvalidToken(ActionInvocation invocation) throws Exception {
Object action = invocation.getAction();
String errorMessage = getErrorMessage(invocation);
String errorMessage = getErrorMessage((org.apache.struts2.ActionInvocation) invocation);
if (action instanceof ValidationAware) {
((ValidationAware) action).addActionError(errorMessage);
@@ -170,6 +167,17 @@ public class TokenInterceptor extends MethodFilterInterceptor {
return INVALID_TOKEN_CODE;
}
/**
* Determines what to do if an invalid token is provided. If the action implements {@link ValidationAware}
*
* @param invocation the action invocation where the invalid token failed
* @return the return code to indicate should be processed
* @throws Exception when any unexpected error occurs.
*/
protected String handleInvalidToken(org.apache.struts2.ActionInvocation invocation) throws Exception {
return handleInvalidToken(ActionInvocation.adapt(invocation));
}
protected String getErrorMessage(ActionInvocation invocation) {
Object action = invocation.getAction();
if (action instanceof TextProvider) {
@@ -178,6 +186,14 @@ public class TokenInterceptor extends MethodFilterInterceptor {
return textProvider.getText(INVALID_TOKEN_MESSAGE_KEY, DEFAULT_ERROR_MESSAGE);
}
protected String getErrorMessage(org.apache.struts2.ActionInvocation invocation) {
return getErrorMessage(ActionInvocation.adapt(invocation));
}
protected String handleValidToken(ActionInvocation invocation) throws Exception {
return invocation.invoke();
}
/**
* Called when a valid token is found. This method invokes the action by can be changed to do something more
* interesting.
@@ -186,8 +202,8 @@ public class TokenInterceptor extends MethodFilterInterceptor {
* @return invocation result
* @throws Exception when any unexpected error occurs.
*/
protected String handleValidToken(ActionInvocation invocation) throws Exception {
return invocation.invoke();
protected String handleValidToken(org.apache.struts2.ActionInvocation invocation) throws Exception {
return handleValidToken(ActionInvocation.adapt(invocation));
}
}
@@ -90,7 +90,8 @@ public class HttpMethodInterceptor extends AbstractInterceptor {
invocation.getProxy().getMethod(), AllowedHttpMethod.class.getSimpleName(), request.getMethod());
return doIntercept(invocation, method);
}
} else if (AnnotationUtils.isAnnotatedBy(action.getClass(), HTTP_METHOD_ANNOTATIONS)) {
}
if (AnnotationUtils.isAnnotatedBy(action.getClass(), HTTP_METHOD_ANNOTATIONS)) {
LOG.debug("Action: {} annotated with: {}, checking if request: {} meets allowed methods!",
action, AllowedHttpMethod.class.getSimpleName(), request.getMethod());
return doIntercept(invocation, action.getClass());
@@ -21,6 +21,7 @@ package org.apache.struts2.result;
import com.opensymphony.xwork2.ActionContext;
import com.opensymphony.xwork2.ActionInvocation;
import com.opensymphony.xwork2.inject.Inject;
import org.apache.commons.text.StringEscapeUtils;
import org.apache.struts2.dispatcher.mapper.ActionMapper;
import org.apache.struts2.dispatcher.mapper.ActionMapping;
@@ -101,7 +102,7 @@ public class PostbackResult extends StrutsResultSupport {
// Render
PrintWriter pw = new PrintWriter(response.getOutputStream());
pw.write("<!DOCTYPE html><html><body><form action=\"" + finalLocation + "\" method=\"POST\">");
pw.write("<!DOCTYPE html><html><body><form action=\"" + StringEscapeUtils.escapeHtml4(finalLocation) + "\" method=\"POST\">");
writeFormElements(request, pw);
writePrologueScript(pw);
pw.write("</html>");
@@ -21,6 +21,7 @@ package org.apache.struts2.result;
import com.opensymphony.xwork2.ActionInvocation;
import com.opensymphony.xwork2.inject.Inject;
import com.opensymphony.xwork2.security.NotExcludedAcceptedPatternsChecker;
import org.apache.commons.lang3.StringUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
@@ -223,7 +224,7 @@ public class StreamResult extends StrutsResultSupport {
if (inputStream == null) {
String msg = ("Can not find a java.io.InputStream with the name [" + parsedInputName + "] in the invocation stack. " +
"Check the <param name=\"inputName\"> tag specified for this action is correct, not excluded and accepted.");
"Check the <param name=\"inputName\"> tag specified for this action is correct, not excluded and accepted.");
LOG.error(msg);
throw new IllegalArgumentException(msg);
}
@@ -231,11 +232,12 @@ public class StreamResult extends StrutsResultSupport {
HttpServletResponse oResponse = invocation.getInvocationContext().getServletResponse();
LOG.debug("Set the content type: {};charset{}", contentType, contentCharSet);
if (contentCharSet != null && !contentCharSet.equals("")) {
oResponse.setContentType(conditionalParse(contentType, invocation) + ";charset=" + conditionalParse(contentCharSet, invocation));
} else {
oResponse.setContentType(conditionalParse(contentType, invocation));
LOG.debug("Set the content type: {};charset={}", contentType, contentCharSet);
String parsedContentType = conditionalParse(contentType, invocation);
String parsedContentCharSet = conditionalParse(contentCharSet, invocation);
oResponse.setContentType(parsedContentType);
if (StringUtils.isNotEmpty(parsedContentCharSet)) {
oResponse.setCharacterEncoding(parsedContentCharSet);
}
LOG.debug("Set the content length: {}", contentLength);
@@ -267,7 +269,7 @@ public class StreamResult extends StrutsResultSupport {
oOutput = oResponse.getOutputStream();
LOG.debug("Streaming result [{}] type=[{}] length=[{}] content-disposition=[{}] charset=[{}]",
inputName, contentType, contentLength, contentDisposition, contentCharSet);
inputName, contentType, contentLength, contentDisposition, contentCharSet);
LOG.debug("Streaming to output buffer +++ START +++");
byte[] oBuff = new byte[bufferSize];
@@ -55,7 +55,7 @@ public class InvocationSessionStore {
return null;
}
final ActionInvocation savedInvocation = invocationContext.invocation;
final ActionInvocation savedInvocation = ActionInvocation.adapt(invocationContext.invocation);
if (savedInvocation != null) {
// WW-5026 - Preserve the previous PageContext (even if null) and restore it to the
// ActionContext after loading the savedInvocation context. The saved context's PageContext
@@ -72,6 +72,10 @@ public class InvocationSessionStore {
return savedInvocation;
}
public static void storeInvocation(String key, String token, ActionInvocation invocation) {
storeInvocation(key, token, (org.apache.struts2.ActionInvocation) invocation);
}
/**
* Stores the DefaultActionInvocation and ActionContext into the Session using the provided key for loading later using
* {@link #loadInvocation}
@@ -80,7 +84,7 @@ public class InvocationSessionStore {
* @param token token for check
* @param invocation the action invocation
*/
public static void storeInvocation(String key, String token, ActionInvocation invocation) {
public static void storeInvocation(String key, String token, org.apache.struts2.ActionInvocation invocation) {
InvocationContext invocationContext = new InvocationContext(invocation, token);
Map<String, Object> invocationMap = getInvocationMap();
invocationMap.put(key, invocationContext);
@@ -120,11 +124,11 @@ public class InvocationSessionStore {
private static final long serialVersionUID = -286697666275777888L;
//WW-4873 transient since 2.5.15
transient ActionInvocation invocation;
transient org.apache.struts2.ActionInvocation invocation;
String token;
public InvocationContext(ActionInvocation invocation, String token) {
public InvocationContext(org.apache.struts2.ActionInvocation invocation, String token) {
this.invocation = invocation;
this.token = token;
}
@@ -24,6 +24,9 @@
### This can be used to set your default locale and encoding scheme
# struts.locale=en_US
### When true, restrict request-derived locales (Accept-Language, used when struts.locale is unset) to the
### JVM's available-locale set; unavailable values fall back to the default locale. Defaults to false.
struts.locale.validateRequestLocale=false
struts.i18n.encoding=UTF-8
### if specified, the default object factory can be overridden here
@@ -240,6 +243,13 @@ struts.ognl.expressionCacheType=wtlfu
### chosen and application-specific needs.
struts.ognl.expressionCacheMaxSize=10000
### Specifies the type of cache to use for the localized-text provider caches. See StrutsConstants for details.
struts.i18n.cacheType=wtlfu
### Specifies the maximum size of each localized-text provider cache. This should be configured based on the
### cache type chosen and application-specific needs.
struts.i18n.cacheMaxSize=10000
### Specifies the type of cache to use for BeanInfo objects. See StrutsConstants class for further information.
struts.ognl.beanInfoCacheType=wtlfu
@@ -247,6 +257,13 @@ struts.ognl.beanInfoCacheType=wtlfu
### application-specific needs.
struts.ognl.beanInfoCacheMaxSize=10000
### Specifies the type of cache to use for proxy detection in ProxyUtil.
### Valid values: basic, lru, wtlfu. Default is 'wtlfu'.
struts.proxy.cacheType=wtlfu
### Specifies the maximum cache size for proxy detection caches.
struts.proxy.cacheMaxSize=10000
### 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
@@ -283,4 +300,8 @@ struts.url.decoder=strutsUrlDecoder
### Defines source to read nonce value from, possible values are: request, session
struts.csp.nonceSource=session
### Maximum size, in characters, of a CSP violation report accepted by CspReportAction
### Reports larger than this are discarded. Values outside 1..1048576 are ignored.
struts.csp.report.maxSize=8192
### END SNIPPET: complete_file
+24
View File
@@ -79,6 +79,8 @@
class="org.apache.struts2.dispatcher.mapper.CompositeActionMapper"/>
<bean type="org.apache.struts2.dispatcher.mapper.ActionMapper" name="prefix"
class="org.apache.struts2.dispatcher.mapper.PrefixBasedActionMapper"/>
<!-- Deprecated for removal: the legacy "restful" and "restful2" mappers predate the Struts REST
plugin, which is the maintained way to build REST-style applications. -->
<bean type="org.apache.struts2.dispatcher.mapper.ActionMapper" name="restful"
class="org.apache.struts2.dispatcher.mapper.RestfulActionMapper"/>
<bean type="org.apache.struts2.dispatcher.mapper.ActionMapper" name="restful2"
@@ -114,6 +116,9 @@
class="org.apache.struts2.conversion.StrutsTypeConverterCreator"/>
<bean type="com.opensymphony.xwork2.conversion.TypeConverterHolder" name="struts"
class="org.apache.struts2.conversion.StrutsTypeConverterHolder"/>
<bean type="org.apache.struts2.conversion.UserConversionPropertiesProvider" name="struts"
class="org.apache.struts2.conversion.StrutsConversionPropertiesProcessor"/>
<bean class="org.apache.struts2.conversion.UserConversionPropertiesProcessor" scope="singleton"/>
<bean class="com.opensymphony.xwork2.conversion.impl.XWorkBasicConverter"/>
@@ -252,6 +257,9 @@
class="com.opensymphony.xwork2.ognl.DefaultOgnlExpressionCacheFactory" scope="singleton"/>
<bean type="com.opensymphony.xwork2.ognl.BeanInfoCacheFactory" name="struts"
class="com.opensymphony.xwork2.ognl.DefaultOgnlBeanInfoCacheFactory" scope="singleton"/>
<bean type="com.opensymphony.xwork2.ognl.ProxyCacheFactory" name="struts"
class="com.opensymphony.xwork2.ognl.StrutsProxyCacheFactory" scope="singleton"/>
<bean class="com.opensymphony.xwork2.util.StrutsProxyCacheFactoryBean" scope="singleton"/>
<bean type="org.apache.struts2.url.QueryStringBuilder" name="strutsQueryStringBuilder"
class="org.apache.struts2.url.StrutsQueryStringBuilder" scope="singleton"/>
@@ -268,4 +276,20 @@
<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="com.opensymphony.xwork2.ognl.accessor.CompoundRootAccessor"/>
<bean type="org.apache.struts2.dispatcher.InternalDestroyable" name="defaultFileManager"
class="com.opensymphony.xwork2.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"/>
</struts>
@@ -18,15 +18,14 @@
*/
package com.opensymphony.xwork2;
import com.opensymphony.xwork2.config.ConfigurationException;
import com.opensymphony.xwork2.mock.MockActionInvocation;
import org.apache.struts2.StrutsInternalTestCase;
import org.apache.struts2.config.StrutsXmlConfigurationProvider;
import org.junit.Test;
public class DefaultActionProxyTest extends StrutsInternalTestCase {
@Test
public void testThorwExceptionOnNotAllowedMethod() throws Exception {
public void testThrowExceptionOnNotAllowedMethod() {
final String filename = "com/opensymphony/xwork2/config/providers/xwork-test-allowed-methods.xml";
loadConfigurationProviders(new StrutsXmlConfigurationProvider(filename));
DefaultActionProxy dap = new DefaultActionProxy(new MockActionInvocation(), "strict", "Default", "notAllowed", true, true);
@@ -35,8 +34,52 @@ public class DefaultActionProxyTest extends StrutsInternalTestCase {
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() {
final String filename = "com/opensymphony/xwork2/config/providers/xwork-test-allowed-methods.xml";
loadConfigurationProviders(new StrutsXmlConfigurationProvider(filename));
DefaultActionProxy dap = new DefaultActionProxy(new MockActionInvocation(), "", "NoMethod", "onPostOnly", true, true);
container.inject(dap);
dap.prepare();
assertTrue("Method passed explicitly should be marked as specified", dap.isMethodSpecified());
assertEquals("onPostOnly", dap.getMethod());
}
public void testMethodSpecifiedWhenResolvedFromConfig() {
final String filename = "com/opensymphony/xwork2/config/providers/xwork-test-allowed-methods.xml";
loadConfigurationProviders(new StrutsXmlConfigurationProvider(filename));
DefaultActionProxy dap = new DefaultActionProxy(new MockActionInvocation(), "", "ConfigMethod", null, true, true);
container.inject(dap);
dap.prepare();
assertTrue("Method resolved from action config should be marked as specified", dap.isMethodSpecified());
assertEquals("onPostOnly", dap.getMethod());
}
public void testMethodNotSpecifiedWhenDefaultingToExecute() {
final String filename = "com/opensymphony/xwork2/config/providers/xwork-test-allowed-methods.xml";
loadConfigurationProviders(new StrutsXmlConfigurationProvider(filename));
DefaultActionProxy dap = new DefaultActionProxy(new MockActionInvocation(), "", "NoMethod", null, true, true);
container.inject(dap);
dap.prepare();
assertFalse("Method defaulting to execute should not be marked as specified", dap.isMethodSpecified());
assertEquals("execute", dap.getMethod());
}
public void testMethodSpecifiedWithWildcardAction() {
final String filename = "com/opensymphony/xwork2/config/providers/xwork-test-allowed-methods.xml";
loadConfigurationProviders(new StrutsXmlConfigurationProvider(filename));
DefaultActionProxy dap = new DefaultActionProxy(new MockActionInvocation(), "", "Wild-onPostOnly", null, true, true);
container.inject(dap);
dap.prepare();
assertTrue("Method resolved from wildcard should be marked as specified", dap.isMethodSpecified());
assertEquals("onPostOnly", dap.getMethod());
}
}
@@ -338,6 +338,26 @@ public class ConfigurationTest extends XWorkTestCase {
}
public void testDefaultActionRefWithWildcard() {
RuntimeConfiguration runtimeConfiguration = configurationManager.getConfiguration().getRuntimeConfiguration();
ActionConfig config = runtimeConfiguration.getActionConfig("/wildcard-default", "unmatchedAction");
assertNotNull("Wildcard default action ref should resolve via wildcard matching", config);
assertEquals("com.opensymphony.xwork2.SimpleAction", config.getClassName());
}
public void testDefaultActionRefWithExactMatch() {
RuntimeConfiguration runtimeConfiguration = configurationManager.getConfiguration().getRuntimeConfiguration();
ActionConfig config = runtimeConfiguration.getActionConfig("/exact-default", "unmatchedAction");
assertNotNull("Exact default action ref should resolve via exact matching", config);
assertEquals("com.opensymphony.xwork2.SimpleAction", config.getClassName());
}
public void testDefaultActionRefWithWildcardNoMatch() {
RuntimeConfiguration runtimeConfiguration = configurationManager.getConfiguration().getRuntimeConfiguration();
ActionConfig config = runtimeConfiguration.getActionConfig("/wildcard-default-nomatch", "unmatchedAction");
assertNull("Default action ref with no matching action should return null", 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,128 @@
/*
* 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 com.opensymphony.xwork2.conversion.impl;
import com.opensymphony.xwork2.ActionContext;
import com.opensymphony.xwork2.XWorkTestCase;
import com.opensymphony.xwork2.conversion.TypeConverter;
import com.opensymphony.xwork2.util.ValueStack;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
public class CollectionConverterTest extends XWorkTestCase {
/**
* WW-5701: the marker constant's value is ordinary text, so an element that genuinely holds
* that text converts successfully and must be kept.
* <p>
* The value is built at runtime rather than written as a literal on purpose: a literal would be
* interned to the very same instance as the constant's value, which no request-derived
* parameter ever is. A servlet container builds parameter values from the request bytes.
*/
public void testElementWhoseTextEqualsTheMarkerIsKept() {
String asSubmittedByAUser = new String("ognl.NoConversionPossible".toCharArray());
assertNotSame("fixture must not be interned", TypeConverter.NO_CONVERSION_POSSIBLE, asSubmittedByAUser);
Holder holder = new Holder();
ValueStack vs = ActionContext.getContext().getValueStack();
vs.push(holder);
vs.setValue("names", new String[]{"alpha", asSubmittedByAUser, "omega"});
assertEquals(Arrays.asList("alpha", "ognl.NoConversionPossible", "omega"), holder.getNames());
}
/**
* The guard must still do its job: a genuinely unconvertible element is dropped.
*/
public void testUnconvertibleElementIsStillDropped() {
Holder holder = new Holder();
ValueStack vs = ActionContext.getContext().getValueStack();
vs.push(holder);
vs.setValue("numbers", new String[]{"1", "not-a-number", "3"});
assertEquals(Arrays.asList(1L, 3L), holder.getNumbers());
}
/**
* The same guard on the path taken when the submitted value is itself a collection rather than
* an array - here a List feeding a Set-typed property.
*/
public void testUnconvertibleElementIsDroppedFromACollectionSource() {
Holder holder = new Holder();
ValueStack vs = ActionContext.getContext().getValueStack();
vs.push(holder);
vs.setValue("numberSet", Arrays.asList("1", "not-a-number", "3"));
assertEquals(new HashSet<>(Arrays.asList(1L, 3L)), holder.getNumberSet());
}
/**
* The same guard on the path taken when a single value is assigned to a collection property.
* The property is seeded first so that a setter which is never called cannot pass vacuously.
*/
public void testUnconvertibleSingleValueIsDropped() {
Holder holder = new Holder();
holder.setNumbers(new ArrayList<>(Arrays.asList(99L)));
ValueStack vs = ActionContext.getContext().getValueStack();
vs.push(holder);
vs.setValue("numbers", "not-a-number");
assertEquals(Collections.emptyList(), holder.getNumbers());
}
public static class Holder {
private List<String> names = new ArrayList<>();
private List<Long> numbers = new ArrayList<>();
private Set<Long> numberSet = new LinkedHashSet<>();
public List<String> getNames() {
return names;
}
public void setNames(List<String> names) {
this.names = names;
}
public List<Long> getNumbers() {
return numbers;
}
public void setNumbers(List<Long> numbers) {
this.numbers = numbers;
}
public Set<Long> getNumberSet() {
return numberSet;
}
public void setNumberSet(Set<Long> numberSet) {
this.numberSet = numberSet;
}
}
}
@@ -22,6 +22,7 @@ import org.apache.commons.lang3.StringUtils;
import org.apache.struts2.StrutsInternalTestCase;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.Locale;
import java.util.Map;
@@ -102,6 +103,18 @@ public class StringConverterTest extends StrutsInternalTestCase {
assertEquals(aBitBiggerThanDouble.substring(0, 309) + "," + aBitBiggerThanDouble.substring(310), value);
}
public void testBigDecimalFractionDigitsAreBounded() throws Exception {
// given
StringConverter converter = new StringConverter();
Map<String, Object> context = createContextWithLocale(new Locale("pl", "PL"));
// when the scale of the value exceeds the supported number of fraction digits
Object value = converter.convertValue(context, null, null, null, new BigDecimal(BigInteger.ONE, 100_000), null);
// then the length of the output is bounded by the converter, not by the scale of the value
assertEquals("0", value);
}
public void testStringArrayToStringConversion() {
// given
StringConverter converter = new StringConverter();
@@ -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 com.opensymphony.xwork2.inject.util;
import org.junit.Test;
import java.lang.ref.ReferenceQueue;
import static org.junit.Assert.assertNull;
public class FinalizableReferenceQueueTest {
@Test
public void stopAndClearIsIdempotent() {
// Should not throw even when called multiple times
FinalizableReferenceQueue.stopAndClear();
FinalizableReferenceQueue.stopAndClear();
ReferenceQueue<Object> instance = FinalizableReferenceQueue.getInstance();
assertNull("FinalizableReferenceQueue instance should be null after stopAndClear", instance);
}
}
@@ -0,0 +1,50 @@
/*
* 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 com.opensymphony.xwork2.ognl;
import org.junit.Test;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
public class OgnlCacheRemoveTest {
private void assertRemoveContract(OgnlCache<String, String> cache) {
cache.put("k", "v");
assertEquals("v", cache.get("k"));
assertEquals("remove returns previous value", "v", cache.remove("k"));
assertNull("entry gone after remove", cache.get("k"));
assertNull("remove of absent key returns null", cache.remove("absent"));
}
@Test
public void caffeineCacheRemove() {
assertRemoveContract(new OgnlCaffeineCache<>(10, 16));
}
@Test
public void defaultCacheRemove() {
assertRemoveContract(new OgnlDefaultCache<>(10, 16, 0.75f));
}
@Test
public void lruCacheRemove() {
assertRemoveContract(new OgnlLRUCache<>(10, 16, 0.75f));
}
}
@@ -64,6 +64,22 @@ public class XWorkListPropertyAccessorTest extends XWorkTestCase {
assertEquals(myList.size(), vs.findValue("strings.size"));
}
public void testUnconvertibleElementIsNotStored() {
ValueStack vs = ActionContext.getContext().getValueStack();
ListHolder listHolder = new ListHolder();
listHolder.setLongs(new ArrayList<>());
vs.push(listHolder);
vs.setValue("longs[0]", "1");
vs.setValue("longs[1]", "not-a-number");
assertEquals(Long.valueOf(1), listHolder.getLongs().get(0));
for (Object element : (List) listHolder.getLongs()) {
assertTrue("list must not hold a non-Long element: " + element,
element == null || element instanceof Long);
}
}
public void testAutoGrowthCollectionLimit() {
PropertyAccessor accessor = container.getInstance(PropertyAccessor.class, ArrayList.class.getName());
((XWorkListPropertyAccessor) accessor).setAutoGrowCollectionLimit("2");
@@ -25,6 +25,7 @@ import com.opensymphony.xwork2.util.ValueStack;
import com.opensymphony.xwork2.util.reflection.ReflectionContextState;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
public class XWorkMapPropertyAccessorTest extends XWorkTestCase {
@@ -57,6 +58,50 @@ public class XWorkMapPropertyAccessorTest extends XWorkTestCase {
assertNull(vs.findValue("map['key']"));
}
public void testUnconvertibleValueIsNotStored() {
TypedMapHolder holder = new TypedMapHolder();
ValueStack vs = ActionContext.getContext().getValueStack();
vs.push(holder);
vs.setValue("counts[1]", "5");
vs.setValue("counts[2]", "not-a-number");
assertEquals(Integer.valueOf(5), holder.getCounts().get(1L));
assertOnlyDeclaredTypes(holder.getCounts());
}
public void testUnconvertibleKeyIsNotStored() {
TypedMapHolder holder = new TypedMapHolder();
ValueStack vs = ActionContext.getContext().getValueStack();
vs.push(holder);
vs.setValue("counts[1]", "5");
vs.setValue("counts['abc']", "6");
assertEquals(Integer.valueOf(5), holder.getCounts().get(1L));
assertOnlyDeclaredTypes(holder.getCounts());
}
/**
* A Map declared to hold Long keys and Integer values must never be left holding anything else.
*/
private static void assertOnlyDeclaredTypes(Map<Long, Integer> map) {
for (Object o : ((Map) map).entrySet()) {
Map.Entry entry = (Map.Entry) o;
assertTrue("key is not a Long: " + entry.getKey(), entry.getKey() instanceof Long);
assertTrue("value is not an Integer: " + entry.getValue(), entry.getValue() instanceof Integer);
}
}
public static class TypedMapHolder {
@Element(value = Integer.class)
private final Map<Long, Integer> counts = new HashMap<>();
public Map<Long, Integer> getCounts() {
return counts;
}
}
private static class MapHolder {
private final Map map;
@@ -0,0 +1,28 @@
/*
* 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 com.opensymphony.xwork2.util;
/**
* Simple fixture whose class-associated bundle ({@code CacheFixture.properties}) backs the
* localized-text caching tests.
*
* @since 6.11.0
*/
public class CacheFixture {
}
@@ -0,0 +1,255 @@
/*
* 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 com.opensymphony.xwork2.util;
import com.github.benmanes.caffeine.cache.Cache;
import com.opensymphony.xwork2.config.ConfigurationException;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import java.lang.reflect.Field;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Set;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
public class ConfigParseUtilTest {
@Before
public void setUp() {
validatedClassCache().invalidateAll();
}
@After
public void tearDown() {
validatedClassCache().invalidateAll();
}
/**
* (a) Single-loader caching: one loader validates several distinct classes; repeating the call
* loads each class exactly once. Covers both "repeated calls hit the cache" and "the inner cache
* is keyed per class name".
*/
@Test
public void testSameLoaderCachesEachDistinctClassOnce() {
CountingClassLoader loader = new CountingClassLoader(getClass().getClassLoader(), "single-loader");
Set<String> classNames = new HashSet<>();
classNames.add(String.class.getName());
classNames.add(Integer.class.getName());
classNames.add(Boolean.class.getName());
ConfigParseUtil.validateClasses(classNames, loader);
ConfigParseUtil.validateClasses(classNames, loader);
assertEquals(1, loader.getLoadCount(String.class.getName()));
assertEquals(1, loader.getLoadCount(Integer.class.getName()));
assertEquals(1, loader.getLoadCount(Boolean.class.getName()));
}
/**
* (b) Per-loader isolation: the outer cache is keyed by classloader identity, not by toString().
* Two loaders that share the same toString() each load the class once, and re-validating one
* loader still hits its own cache.
*/
@Test
public void testDifferentLoadersWithSameNameCacheIndependently() {
CountingClassLoader firstLoader = new CountingClassLoader(getClass().getClassLoader(), "same-name");
CountingClassLoader secondLoader = new CountingClassLoader(getClass().getClassLoader(), "same-name");
Set<String> classNames = Collections.singleton(String.class.getName());
ConfigParseUtil.validateClasses(classNames, firstLoader);
ConfigParseUtil.validateClasses(classNames, secondLoader);
assertEquals(1, firstLoader.getStringClassLoads());
assertEquals(1, secondLoader.getStringClassLoads());
// Re-validating the first loader still hits its own cache.
ConfigParseUtil.validateClasses(classNames, firstLoader);
assertEquals(1, firstLoader.getStringClassLoads());
}
/**
* Negative case: a missing class throws ConfigurationException (cause ClassNotFoundException) on
* every call, and the failure is not cached (each call re-attempts the load).
*/
@Test
public void testMissingClassThrowsAndIsNotCached() {
String missingClassName = "com.opensymphony.xwork2.util.NonExistingClassForValidationTest";
Set<String> classNames = Collections.singleton(missingClassName);
int[] missingClassLoads = new int[1];
ClassLoader loader = new ClassLoader(getClass().getClassLoader()) {
@Override
public Class<?> loadClass(String name) throws ClassNotFoundException {
if (missingClassName.equals(name)) {
missingClassLoads[0]++;
throw new ClassNotFoundException(name);
}
return super.loadClass(name);
}
@Override
public String toString() {
return "missing-class-loader";
}
};
for (int i = 0; i < 2; i++) {
try {
ConfigParseUtil.validateClasses(classNames, loader);
fail("Expected ConfigurationException for class: " + missingClassName);
} catch (ConfigurationException e) {
assertTrue(e.getMessage().contains(missingClassName));
assertNotNull(e.getCause());
assertEquals(ClassNotFoundException.class, e.getCause().getClass());
}
}
assertEquals(2, missingClassLoads[0]);
}
/**
* (c) Outer cache bound: registering more classloaders than the maximum keeps the outer cache at
* or below its configured size.
*/
@Test
public void testOuterCacheBoundedByMaxClassloaders() {
Set<String> classNames = Collections.singleton(String.class.getName());
for (int i = 0; i < outerCacheLimit() + 10; i++) {
CountingClassLoader loader = new CountingClassLoader(getClass().getClassLoader(), "loader-" + i);
ConfigParseUtil.validateClasses(classNames, loader);
}
Cache<Object, Object> cache = validatedClassCache();
cache.cleanUp();
assertTrue("Outer cache size should not exceed configured maximum",
cache.estimatedSize() <= outerCacheLimit());
}
/**
* (c) Inner cache bound: validating more class names than the per-loader maximum keeps that
* loader's inner cache at or below its configured size. Synthetic names are resolved to a real
* class so the count is driven by distinct keys, not by which JDK classes happen to exist.
*/
@Test
public void testInnerCacheBoundedByMaxClassesPerLoader() {
int limit = innerCacheLimit();
ClassLoader loader = new ClassLoader(getClass().getClassLoader()) {
@Override
public Class<?> loadClass(String name) {
// Resolve any synthetic name to a strongly-reachable class so weakValues never evicts it.
return Object.class;
}
@Override
public String toString() {
return "inner-bound-loader";
}
};
Set<String> classNames = new LinkedHashSet<>();
for (int i = 0; i <= limit + 10; i++) {
classNames.add("synthetic.Class" + i);
}
assertTrue("Test must request more class names than the inner cache capacity",
classNames.size() > limit);
ConfigParseUtil.validateClasses(classNames, loader);
Cache<Object, Object> innerCache = innerCacheFor(loader);
innerCache.cleanUp();
assertTrue("Inner cache size should not exceed configured maximum per loader",
innerCache.estimatedSize() <= limit);
}
@SuppressWarnings("unchecked")
private static Cache<Object, Object> validatedClassCache() {
try {
Field cacheField = ConfigParseUtil.class.getDeclaredField("VALIDATED_CLASS_CACHE");
cacheField.setAccessible(true);
return (Cache<Object, Object>) cacheField.get(null);
} catch (NoSuchFieldException | IllegalAccessException e) {
throw new AssertionError("Cannot access ConfigParseUtil cache field", e);
}
}
@SuppressWarnings("unchecked")
private static Cache<Object, Object> innerCacheFor(ClassLoader loader) {
Cache<Object, Object> outer = validatedClassCache();
Object inner = outer.getIfPresent(loader);
assertNotNull("Expected an inner cache entry for loader", inner);
return (Cache<Object, Object>) inner;
}
private static int outerCacheLimit() {
return intConstant("MAX_CLASSLOADER_CACHE_SIZE");
}
private static int innerCacheLimit() {
return intConstant("MAX_CLASS_CACHE_PER_LOADER_SIZE");
}
private static int intConstant(String fieldName) {
try {
Field field = ConfigParseUtil.class.getDeclaredField(fieldName);
field.setAccessible(true);
return field.getInt(null);
} catch (NoSuchFieldException | IllegalAccessException e) {
throw new AssertionError("Cannot access ConfigParseUtil constant: " + fieldName, e);
}
}
private static final class CountingClassLoader extends ClassLoader {
private final String loaderName;
private final Map<String, Integer> loadCounts = new HashMap<>();
private CountingClassLoader(ClassLoader parent, String loaderName) {
super(parent);
this.loaderName = loaderName;
}
@Override
public Class<?> loadClass(String name) throws ClassNotFoundException {
loadCounts.merge(name, 1, Integer::sum);
return super.loadClass(name);
}
private int getStringClassLoads() {
return loadCounts.getOrDefault(String.class.getName(), 0);
}
private int getLoadCount(String className) {
return loadCounts.getOrDefault(className, 0);
}
@Override
public String toString() {
return loaderName;
}
}
}
@@ -24,17 +24,18 @@ import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;
import org.xml.sax.InputSource;
import org.xml.sax.SAXParseException;
import java.io.StringReader;
import java.util.Objects;
/**
* Test cases for {@link DomHelper}.
*/
public class DomHelperTest extends TestCase {
private final String xml = "<!DOCTYPE foo [<!ELEMENT foo (bar)><!ELEMENT bar (#PCDATA)>]>\n<foo>\n<bar/>\n</foo>\n";
public void testParse() {
String xml = "<!DOCTYPE foo [<!ELEMENT foo (bar)><!ELEMENT bar (#PCDATA)>]>\n<foo>\n<bar/>\n</foo>\n";
InputSource in = new InputSource(new StringReader(xml));
in.setSystemId("foo://bar");
@@ -47,6 +48,7 @@ public class DomHelperTest extends TestCase {
}
public void testGetLocationObject() {
String xml = "<!DOCTYPE foo [<!ELEMENT foo (bar)><!ELEMENT bar (#PCDATA)>]>\n<foo>\n<bar/>\n</foo>\n";
InputSource in = new InputSource(new StringReader(xml));
in.setSystemId("foo://bar");
@@ -61,7 +63,7 @@ public class DomHelperTest extends TestCase {
}
public void testExternalEntities() {
String dtdFile = getClass().getResource("/author.dtd").getPath();
String dtdFile = Objects.requireNonNull(getClass().getResource("/author.dtd")).getPath();
String xml = "<!DOCTYPE foo [<!ELEMENT foo (bar)><!ELEMENT bar (#PCDATA)><!ENTITY writer SYSTEM \"file://" + dtdFile + "\">]><foo><bar>&writer;</bar></foo>";
InputSource in = new InputSource(new StringReader(xml));
in.setSystemId("foo://bar");
@@ -74,4 +76,34 @@ public class DomHelperTest extends TestCase {
assertEquals(1, nl.getLength());
assertNull(nl.item(0).getNodeValue());
}
/**
* Tests that the parser is protected against Billion Laughs (XML Entity Expansion) attack.
* The FEATURE_SECURE_PROCESSING flag and the JDK's built-in entity expansion limit (64K
* since JDK 7u45) both cap entity expansion to prevent DoS.
* See: <a href="https://en.wikipedia.org/wiki/Billion_laughs_attack">Billion laughs attack</a>
*/
public void testBillionLaughsProtection() {
String xml = "<?xml version=\"1.0\"?>" +
"<!DOCTYPE root [" +
"<!ENTITY lol0 \"lol\">" +
"<!ENTITY lol1 \"&lol0;&lol0;&lol0;&lol0;&lol0;&lol0;&lol0;&lol0;&lol0;&lol0;\">" +
"<!ENTITY lol2 \"&lol1;&lol1;&lol1;&lol1;&lol1;&lol1;&lol1;&lol1;&lol1;&lol1;\">" +
"<!ENTITY lol3 \"&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;\">" +
"<!ENTITY lol4 \"&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;\">" +
"<!ENTITY lol5 \"&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;\">" +
"]>" +
"<root>&lol5;</root>";
InputSource in = new InputSource(new StringReader(xml));
in.setSystemId("test://billion-laughs");
try {
DomHelper.parse(in);
fail("Parser should reject excessive entity expansion");
} catch (Exception e) {
assertNotNull(e.getCause());
assertTrue(e.getCause() instanceof SAXParseException);
}
}
}
@@ -34,6 +34,11 @@ import com.opensymphony.xwork2.test.TestBean2;
import org.apache.struts2.config.StrutsXmlConfigurationProvider;
import org.apache.struts2.interceptor.parameter.StrutsParameter;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.text.DateFormat;
import java.text.ParseException;
import java.util.Date;
@@ -563,6 +568,109 @@ public class StrutsLocalizedTextProviderTest extends XWorkTestCase {
assertEquals("Result of bean2.name lookup not as expected ?", "Okay! You found Me!", messageResult);
}
public void testCachesAreBoundedByConfiguredMaxSize() {
TestStrutsLocalizedTextProvider provider = new TestStrutsLocalizedTextProvider();
provider.setI18nCacheMaxSize("100");
ValueStack valueStack = ActionContext.getContext().getValueStack();
for (int i = 0; i < 20000; i++) {
Locale locale = Locale.forLanguageTag("en-US-x" + String.format("%05d", i));
provider.findText(CacheFixture.class, "cache.missing", locale, "Fallback", null, valueStack);
}
assertTrue("bundlesMap not bounded ?", provider.bundlesMapSize() <= 2000);
assertTrue("missingBundles not bounded ?", provider.missingBundlesSize() <= 2000);
assertTrue("messageFormats not bounded ?", provider.messageFormatsSize() <= 2000);
}
public void testCorrectTextStillReturnedUnderEviction() {
TestStrutsLocalizedTextProvider provider = new TestStrutsLocalizedTextProvider();
provider.setI18nCacheMaxSize("50");
ValueStack valueStack = ActionContext.getContext().getValueStack();
// Force heavy eviction with many distinct locales.
for (int i = 0; i < 5000; i++) {
Locale locale = Locale.forLanguageTag("en-US-x" + String.format("%05d", i));
provider.findText(CacheFixture.class, "cache.missing", locale, "Fallback", null, valueStack);
}
// A real key in a real locale still resolves correctly after eviction pressure.
String result = provider.findText(CacheFixture.class, "cache.static", Locale.ENGLISH, null, null, valueStack);
assertEquals("Static cached value", result);
}
public void testReloadClearsBoundedCaches() {
TestStrutsLocalizedTextProvider provider = new TestStrutsLocalizedTextProvider();
ValueStack valueStack = ActionContext.getContext().getValueStack();
provider.findText(CacheFixture.class, "cache.missing", Locale.ENGLISH, "Fallback", null, valueStack);
assertTrue("missingBundles not populated ?", provider.missingBundlesSize() > 0);
provider.callReloadBundlesForceReload();
assertEquals("reload did not clear bundlesMap ?", 0, provider.bundlesMapSize());
}
public void testProviderIsUsableAfterDeserialization() throws Exception {
StrutsLocalizedTextProvider provider = new StrutsLocalizedTextProvider();
ValueStack valueStack = ActionContext.getContext().getValueStack();
provider.findText(CacheFixture.class, "cache.static", Locale.ENGLISH, null, null, valueStack);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
try (ObjectOutputStream oos = new ObjectOutputStream(baos)) {
oos.writeObject(provider);
}
Object restored;
try (ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(baos.toByteArray()))) {
restored = ois.readObject();
}
StrutsLocalizedTextProvider deserialized = (StrutsLocalizedTextProvider) restored;
// Caches were transient (null right after defaultReadObject) but readObject rebuilds them:
assertEquals("Deserialized caches not rebuilt empty", 0, deserialized.bundlesMapSize());
String result = deserialized.findText(CacheFixture.class, "cache.static", Locale.ENGLISH, null, null, valueStack);
assertEquals("Static cached value", result);
}
/**
* A stream written before the i18n cache settings existed carries no value for them, and field
* initialisers do not run during deserialization, so they arrive as null/0. The provider must still
* come back usable rather than failing while rebuilding its caches.
*/
public void testProviderIsUsableAfterDeserializingLegacyStream() throws Exception {
StrutsLocalizedTextProvider provider = new StrutsLocalizedTextProvider();
ValueStack valueStack = ActionContext.getContext().getValueStack();
provider.findText(CacheFixture.class, "cache.static", Locale.ENGLISH, null, null, valueStack);
// Simulate the absent-field state an older stream produces.
Field cacheType = AbstractLocalizedTextProvider.class.getDeclaredField("i18nCacheType");
cacheType.setAccessible(true);
cacheType.set(provider, null);
Field maxSize = AbstractLocalizedTextProvider.class.getDeclaredField("i18nCacheMaxSize");
maxSize.setAccessible(true);
maxSize.setInt(provider, 0);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
try (ObjectOutputStream oos = new ObjectOutputStream(baos)) {
oos.writeObject(provider);
}
Object restored;
try (ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(baos.toByteArray()))) {
restored = ois.readObject();
}
StrutsLocalizedTextProvider deserialized = (StrutsLocalizedTextProvider) restored;
String result = deserialized.findText(CacheFixture.class, "cache.static", Locale.ENGLISH, null, null, valueStack);
assertEquals("Static cached value", result);
}
public void testCacheTypeSelectionKeepsProviderWorking() {
TestStrutsLocalizedTextProvider provider = new TestStrutsLocalizedTextProvider();
provider.setI18nCacheType("basic");
ValueStack valueStack = ActionContext.getContext().getValueStack();
String result = provider.findText(CacheFixture.class, "cache.static", Locale.ENGLISH, null, null, valueStack);
assertEquals("Static cached value", result);
assertTrue("bundlesMap should populate", provider.bundlesMapSize() >= 1);
}
@Override
protected void setUp() throws Exception {
super.setUp();
@@ -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.action;
import com.opensymphony.xwork2.XWorkTestCase;
import org.apache.struts2.StrutsConstants;
import org.apache.struts2.interceptor.csp.CspSettings;
import org.springframework.mock.web.MockHttpServletRequest;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.Reader;
import java.util.Properties;
import java.util.concurrent.atomic.AtomicLong;
/**
* Verifies that {@link CspReportAction} applies an upper bound to the report body it accepts, and
* that the bound is configurable.
*/
public class CspReportActionReportSizeTest extends XWorkTestCase {
/**
* The reader supplied by the container buffers ahead, so consumption is bounded by the limit
* plus one buffer rather than by the limit exactly. That overshoot is fixed, not proportional
* to the size of the body.
*/
private static final long READ_AHEAD_ALLOWANCE = 8192L;
/**
* Produces {@code total} characters without buffering them, and records how many the caller
* actually consumed.
*/
private static final class CountingReader extends Reader {
private final long total;
private final AtomicLong consumed;
private long produced = 0;
CountingReader(long total, AtomicLong consumed) {
this.total = total;
this.consumed = consumed;
}
@Override
public int read(char[] cbuf, int off, int len) {
if (produced >= total) {
return -1;
}
int count = (int) Math.min(len, total - produced);
for (int i = 0; i < count; i++) {
cbuf[off + i] = 'a';
}
produced += count;
consumed.addAndGet(count);
return count;
}
@Override
public void close() {
// characters are generated on demand, so there is nothing to release
}
}
private static final class CapturingCspReportAction extends CspReportAction {
String captured;
int reports;
@Override
void processReport(String jsonCspReport) {
captured = jsonCspReport;
reports++;
}
}
/**
* A request that both declares and delivers {@code size} characters, matching what a client can
* actually send: the declared length and the delivered body agree.
*/
private MockHttpServletRequest requestOfSize(final long size, final AtomicLong consumed) {
MockHttpServletRequest request = new MockHttpServletRequest("POST", "/csp-reports") {
@Override
public int getContentLength() {
return (int) Math.min(size, Integer.MAX_VALUE);
}
@Override
public BufferedReader getReader() {
return new BufferedReader(new CountingReader(size, consumed));
}
};
request.setContentType(CspSettings.CSP_REPORT_TYPE);
return request;
}
public void testReportAboveLimitIsNotProcessed() {
AtomicLong consumed = new AtomicLong();
MockHttpServletRequest request = requestOfSize(64L * 1024 * 1024, consumed);
CapturingCspReportAction action = new CapturingCspReportAction();
action.withServletRequest(request);
assertEquals("A report above the limit should not be processed", 0, action.reports);
assertTrue("Consumed " + consumed.get() + " characters for a limit of "
+ CspReportAction.DEFAULT_MAX_REPORT_SIZE,
consumed.get() <= CspReportAction.DEFAULT_MAX_REPORT_SIZE + READ_AHEAD_ALLOWANCE);
}
public void testReportWithinLimitIsProcessed() {
String sampleReport = "{\"csp-report\":{\"document-uri\":\"https://example.test/\"}}";
MockHttpServletRequest request = new MockHttpServletRequest("POST", "/csp-reports");
request.setContent(sampleReport.getBytes());
request.setContentType(CspSettings.CSP_REPORT_TYPE);
CapturingCspReportAction action = new CapturingCspReportAction();
action.withServletRequest(request);
assertEquals("A report within the limit should be processed", 1, action.reports);
assertEquals("The report should be passed through unchanged", sampleReport, action.captured);
}
public void testConfiguredLimitIsApplied() {
AtomicLong consumed = new AtomicLong();
MockHttpServletRequest request = requestOfSize(4096, consumed);
CapturingCspReportAction action = new CapturingCspReportAction();
action.setMaxReportSize("1024");
action.withServletRequest(request);
assertEquals("A report above the configured limit should not be processed", 0, action.reports);
assertTrue("Consumed " + consumed.get() + " characters for a configured limit of 1024",
consumed.get() <= 1024L + READ_AHEAD_ALLOWANCE);
}
/**
* The key named by {@link StrutsConstants#STRUTS_CSP_REPORT_MAX_SIZE} must exist in
* default.properties under exactly that name. If the two drift apart the value is silently never
* injected, leaving the limit hard-coded and the documented setting inert.
*/
public void testLimitKeyIsDefinedInDefaultProperties() throws IOException {
Properties defaults = new Properties();
try (InputStream in = getClass().getClassLoader()
.getResourceAsStream("org/apache/struts2/default.properties")) {
assertNotNull("default.properties should be on the classpath", in);
defaults.load(in);
}
assertEquals(StrutsConstants.STRUTS_CSP_REPORT_MAX_SIZE + " should be defined in default.properties",
String.valueOf(CspReportAction.DEFAULT_MAX_REPORT_SIZE),
defaults.getProperty(StrutsConstants.STRUTS_CSP_REPORT_MAX_SIZE));
}
public void testUnusableConfiguredValuesAreIgnored() {
String[] unusable = {"", " ", "not-a-number", "0", "-1", "2147483647"};
for (String value : unusable) {
AtomicLong consumed = new AtomicLong();
MockHttpServletRequest request = requestOfSize(64L * 1024 * 1024, consumed);
CapturingCspReportAction action = new CapturingCspReportAction();
action.setMaxReportSize(value);
action.withServletRequest(request);
assertEquals("A report above the default limit should not be processed for value '"
+ value + "'", 0, action.reports);
assertTrue("Consumed " + consumed.get() + " characters for value '" + value + "'",
consumed.get() <= CspReportAction.DEFAULT_MAX_REPORT_SIZE + READ_AHEAD_ALLOWANCE);
}
}
}
@@ -0,0 +1,69 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.struts2.conversion;
import com.opensymphony.xwork2.XWorkTestCase;
import com.opensymphony.xwork2.conversion.ConversionPropertiesProcessor;
import com.opensymphony.xwork2.conversion.TypeConverter;
import com.opensymphony.xwork2.conversion.TypeConverterHolder;
import java.io.File;
/**
* Tests for {@link StrutsConversionPropertiesProcessor} two-phase processing.
*
* @see <a href="https://issues.apache.org/jira/browse/WW-4291">WW-4291</a>
*/
public class StrutsConversionPropertiesProcessorTest extends XWorkTestCase {
private TypeConverterHolder converterHolder;
private StrutsConversionPropertiesProcessor processor;
@Override
protected void setUp() throws Exception {
super.setUp();
converterHolder = container.getInstance(TypeConverterHolder.class);
processor = (StrutsConversionPropertiesProcessor) container.getInstance(ConversionPropertiesProcessor.class);
}
/**
* Tests that default converters from struts-default-conversion.properties
* are registered during the early initialization phase.
* java.io.File -> UploadedFileConverter is defined in struts-default-conversion.properties.
*/
public void testDefaultConvertersRegisteredDuringEarlyPhase() {
// The java.io.File converter should be registered from struts-default-conversion.properties
// struts-default-conversion.properties defines: java.io.File=org.apache.struts2.conversion.UploadedFileConverter
TypeConverter fileConverter = converterHolder.getDefaultMapping(File.class.getName());
assertNotNull("java.io.File converter should be registered from default properties", fileConverter);
}
/**
* Tests that the init() method only processes the default conversion properties file.
* User conversion properties should be processed separately via initUserConversions().
*/
public void testInitOnlyProcessesDefaultProperties() {
// This test verifies the behavior is correct - default converters are available
// after bootstrap. The actual split behavior is validated by checking that the
// framework doesn't throw ClassNotFoundException for bean names.
assertNotNull("Processor should be available", processor);
assertNotNull("Converter holder should have default mappings", converterHolder);
}
}
@@ -0,0 +1,227 @@
/*
* 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 com.opensymphony.xwork2.inject.Container;
import com.opensymphony.xwork2.ognl.accessor.CompoundRootAccessor;
import com.opensymphony.xwork2.util.fs.DefaultFileManager;
import org.apache.struts2.StrutsJUnit4InternalTestCase;
import org.apache.struts2.components.Component;
import org.apache.struts2.interceptor.ScopeInterceptor;
import org.junit.Test;
import java.lang.reflect.Field;
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 java.util.concurrent.atomic.AtomicBoolean;
import static java.util.Collections.emptyMap;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
/**
* WW-5537: Verifies that Dispatcher.cleanup() properly clears all static state
* that could prevent classloader garbage collection during hot redeployment.
*/
public class DispatcherCleanupLeakTest 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"
));
assertTrue("All core InternalDestroyable beans should be registered, missing: "
+ missing(expected, names),
names.containsAll(expected));
}
@Test
public void cleanupContinuesWhenDestroyableThrows() {
initDispatcher(emptyMap());
// Populate a cache to verify cleanup still runs after a failure
Field mapField;
try {
mapField = Component.class.getDeclaredField("standardAttributesMap");
mapField.setAccessible(true);
@SuppressWarnings("unchecked")
ConcurrentMap<Class<?>, Collection<String>> map =
(ConcurrentMap<Class<?>, Collection<String>>) mapField.get(null);
map.put(String.class, new ArrayList<>());
assertFalse("Precondition: standardAttributesMap should not be empty", map.isEmpty());
} catch (Exception e) {
throw new RuntimeException(e);
}
// Register a destroyable that throws before other cleanup runs
final AtomicBoolean secondCalled = new AtomicBoolean(false);
InternalDestroyable failing = () -> { throw new RuntimeException("test failure"); };
InternalDestroyable tracking = () -> secondCalled.set(true);
// Call cleanup the loop should catch the exception and continue
Container container = dispatcher.getConfigurationManager().getConfiguration().getContainer();
Set<String> names = container.getInstanceNames(InternalDestroyable.class);
// Simulate the loop with our test destroyables injected
List<InternalDestroyable> destroyables = new ArrayList<>();
destroyables.add(failing);
for (String name : names) {
destroyables.add(container.getInstance(InternalDestroyable.class, name));
}
destroyables.add(tracking);
for (InternalDestroyable d : destroyables) {
try {
d.destroy();
} catch (Exception e) {
// mirrors Dispatcher.cleanup() error handling
}
}
assertTrue("Destroyable after the failing one should still be called", secondCalled.get());
}
@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<>());
assertFalse("Precondition: standardAttributesMap should not be empty", map.isEmpty());
dispatcher.cleanup();
assertTrue("standardAttributesMap should be empty after cleanup", 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);
// Seed with a dummy entry to ensure cleanup actually clears it
invalidMethods.put("testKey", Boolean.TRUE);
assertFalse("Precondition: invalidMethods should not be empty", invalidMethods.isEmpty());
dispatcher.cleanup();
assertTrue("invalidMethods should be empty after cleanup", 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());
assertFalse("Precondition: files should not be empty", files.isEmpty());
dispatcher.cleanup();
assertTrue("DefaultFileManager.files should be empty after cleanup", 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 URL("file:///test"));
assertFalse("Precondition: lazyMonitoredFilesCache should not be empty", lazyCache.isEmpty());
dispatcher.cleanup();
assertTrue("DefaultFileManager.lazyMonitoredFilesCache should be empty after cleanup",
lazyCache.isEmpty());
}
@Test
@SuppressWarnings("unchecked")
public void cleanupClearsScopeInterceptorLocks() throws Exception {
initDispatcher(emptyMap());
Field locksField = ScopeInterceptor.class.getDeclaredField("locks");
locksField.setAccessible(true);
Map<Object, Object> locks = (Map<Object, Object>) locksField.get(null);
locks.put(new Object(), new Object());
assertFalse("Precondition: locks should not be empty", locks.isEmpty());
dispatcher.cleanup();
assertTrue("ScopeInterceptor.locks should be empty after cleanup", locks.isEmpty());
}
@Test
public void cleanupClearsDispatcherListeners() throws Exception {
initDispatcher(emptyMap());
DispatcherListener listener = new DispatcherListener() {
@Override
public void dispatcherInitialized(Dispatcher du) {}
@Override
public void dispatcherDestroyed(Dispatcher du) {}
};
Dispatcher.addDispatcherListener(listener);
dispatcher.cleanup();
Field listenersField = Dispatcher.class.getDeclaredField("dispatcherListeners");
listenersField.setAccessible(true);
List<?> listeners = (List<?>) listenersField.get(null);
assertTrue("dispatcherListeners should be empty after cleanup", listeners.isEmpty());
}
private Set<String> missing(Set<String> expected, Set<String> actual) {
Set<String> diff = new HashSet<>(expected);
diff.removeAll(actual);
return diff;
}
}
@@ -570,6 +570,46 @@ public class DispatcherTest extends StrutsJUnit4InternalTestCase {
assertEquals(Locale.getDefault(), context.getLocale()); // Expect the system default value when Mock request access fails.
}
@Test
public void testValidateRequestLocaleOffPassesThrough() {
initDispatcher(new HashMap<>());
dispatcher.setDefaultLocale(null); // Force struts.locale unset; the test-config default would otherwise mask the request locale.
HttpServletRequest request = mock(HttpServletRequest.class);
// A syntactically valid but not JVM-available locale.
Locale exotic = new Locale("en", "US", "xzz99");
when(request.getLocale()).thenReturn(exotic);
assertEquals("Default off must pass the request locale through unchanged",
exotic, dispatcher.getLocale(request));
}
@Test
public void testValidateRequestLocaleOnKeepsAvailableLocale() {
Map<String, String> params = new HashMap<>();
params.put(StrutsConstants.STRUTS_LOCALE_VALIDATE_REQUEST, "true");
initDispatcher(params);
dispatcher.setDefaultLocale(null); // Force struts.locale unset; the test-config default would otherwise mask the request locale.
HttpServletRequest request = mock(HttpServletRequest.class);
when(request.getLocale()).thenReturn(Locale.UK);
assertEquals("Available request locale must be kept", Locale.UK, dispatcher.getLocale(request));
}
@Test
public void testValidateRequestLocaleOnFallsBackForUnavailableLocale() {
Map<String, String> params = new HashMap<>();
params.put(StrutsConstants.STRUTS_LOCALE_VALIDATE_REQUEST, "true");
initDispatcher(params);
dispatcher.setDefaultLocale(null); // Force struts.locale unset; the test-config default would otherwise mask the request locale.
HttpServletRequest request = mock(HttpServletRequest.class);
Locale exotic = new Locale("en", "US", "xzz99");
when(request.getLocale()).thenReturn(exotic);
// struts.locale unset in this dispatcher -> fall back to the JVM default.
assertEquals("Unavailable request locale must fall back to system default",
Locale.getDefault(), dispatcher.getLocale(request));
}
@Test
public void dispatcherReinjectedAfterReload() {
HttpServletRequest request = mock(HttpServletRequest.class);
@@ -105,6 +105,22 @@ public class RestfulActionMapperTest extends StrutsInternalTestCase {
assertEquals("europe", am.getParams().get("region"));
}
public void testGetMappingRejectsActionNameWithDisallowedCharacters() {
StrutsMockHttpServletRequest request = new StrutsMockHttpServletRequest();
request.setupGetServletPath("/%{1+1}/x");
ActionMapping am = mapper.getMapping(request, null);
assertEquals("index", am.getName());
}
public void testGetMappingAcceptsRegularActionName() {
StrutsMockHttpServletRequest request = new StrutsMockHttpServletRequest();
request.setupGetServletPath("/my-app.action/x");
ActionMapping am = mapper.getMapping(request, null);
assertEquals("my-app.action", am.getName());
}
protected void setUp() throws Exception {
super.setUp();
mapper = new RestfulActionMapper();
@@ -205,7 +205,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);
@@ -294,6 +294,66 @@ public class I18nInterceptorTest extends TestCase {
assertEquals(Locale.US, mai.getInvocationContext().getLocale());
}
public void testRequestLocaleWithSupportedLocale() throws Exception {
// given
interceptor.setSupportedLocale("en,de");
prepare(I18nInterceptor.DEFAULT_PARAMETER, "de");
// when
interceptor.intercept(mai);
// then
Locale german = new Locale("de");
assertEquals(german, session.get(I18nInterceptor.DEFAULT_SESSION_ATTRIBUTE));
assertEquals(german, mai.getInvocationContext().getLocale());
}
public void testUnsupportedRequestLocaleRejected() throws Exception {
// given
interceptor.setSupportedLocale("en,de");
prepare(I18nInterceptor.DEFAULT_PARAMETER, "fr");
// when
interceptor.intercept(mai);
// then - fr is not supported, should fall back to default
assertNull("unsupported locale should not be stored", session.get(I18nInterceptor.DEFAULT_SESSION_ATTRIBUTE));
}
public void testStaleSessionLocaleRejected() throws Exception {
// given - session has a stored locale that is no longer supported
session.put(I18nInterceptor.DEFAULT_SESSION_ATTRIBUTE, Locale.FRENCH);
interceptor.setSupportedLocale("en,de");
// when
interceptor.intercept(mai);
// then - stored fr locale should be discarded since it's not in supportedLocale
assertFalse("stale session locale should be discarded",
Locale.FRENCH.equals(mai.getInvocationContext().getLocale()));
}
public void testCookieRequestLocaleWithSupportedLocale() throws Exception {
// given
interceptor.setSupportedLocale("en,de");
interceptor.setLocaleStorage(I18nInterceptor.Storage.COOKIE.name());
prepare(I18nInterceptor.DEFAULT_COOKIE_PARAMETER, "de");
final Cookie cookie = new Cookie(I18nInterceptor.DEFAULT_COOKIE_ATTRIBUTE, "de");
HttpServletResponse response = EasyMock.createMock(HttpServletResponse.class);
response.addCookie(CookieMatcher.eqCookie(cookie));
EasyMock.replay(response);
ac.put(StrutsStatics.HTTP_RESPONSE, response);
// when
interceptor.intercept(mai);
// then
EasyMock.verify(response);
Locale german = new Locale("de");
assertEquals(german, mai.getInvocationContext().getLocale());
}
private void prepare(String key, Serializable value) {
Map<String, Serializable> params = new HashMap<>();
params.put(key, value);
@@ -308,9 +368,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 +408,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 +419,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());
}
}
@@ -19,13 +19,17 @@
package org.apache.struts2.interceptor.httpmethod;
import com.opensymphony.xwork2.ActionContext;
import com.opensymphony.xwork2.ActionProxy;
import com.opensymphony.xwork2.mock.MockActionInvocation;
import com.opensymphony.xwork2.mock.MockActionProxy;
import org.apache.struts2.HttpMethodsTestAction;
import org.apache.struts2.StrutsInternalTestCase;
import org.apache.struts2.TestAction;
import org.apache.struts2.config.StrutsXmlConfigurationProvider;
import org.springframework.mock.web.MockHttpServletRequest;
import java.util.Map;
public class HttpMethodInterceptorTest extends StrutsInternalTestCase {
private HttpMethodInterceptor interceptor;
@@ -217,6 +221,110 @@ public class HttpMethodInterceptorTest extends StrutsInternalTestCase {
assertEquals(HttpMethod.POST, action.getHttpMethod());
}
public void testWildcardResolvedMethodWithPostAnnotationRejectsGet() throws Exception {
// given
HttpMethodsTestAction action = new HttpMethodsTestAction();
prepareActionInvocation(action);
actionProxy.setMethod("onPostOnly");
actionProxy.setMethodSpecified(true);
invocation.setResultCode("onPostOnly");
prepareRequest("GET");
// when
String resultName = interceptor.intercept(invocation);
// then
assertEquals("bad-request", resultName);
}
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
assertEquals("onPostOnly", resultName);
assertEquals(HttpMethod.POST, action.getHttpMethod());
}
/**
* Regression for wildcard-resolved methods with no method-level HTTP annotation:
* a class-level {@code @AllowedHttpMethod(POST)} must still cause GET to be rejected.
* Previously the interceptor's {@code if/else-if} structure made the class-level
* branch unreachable when {@code isMethodSpecified()=true} and the resolved method
* carried no annotation of its own.
*/
public void testWildcardResolvedUnannotatedMethodRespectsClassLevelAnnotation() throws Exception {
HttpMethodsTestAction action = new HttpMethodsTestAction();
prepareActionInvocation(action);
actionProxy.setMethod("execute");
actionProxy.setMethodSpecified(true);
prepareRequest("get");
String resultName = interceptor.intercept(invocation);
assertEquals("bad-request", resultName);
}
/**
* Counterpart to the above: POST against a wildcard-resolved unannotated method must succeed
* when the class allows POST via {@code @AllowedHttpMethod(POST)}.
*/
public void testWildcardResolvedUnannotatedMethodAllowsPostWithClassLevelAnnotation() throws Exception {
HttpMethodsTestAction action = new HttpMethodsTestAction();
prepareActionInvocation(action);
actionProxy.setMethod("execute");
actionProxy.setMethodSpecified(true);
invocation.setResultCode("success");
prepareRequest("post");
String resultName = interceptor.intercept(invocation);
assertEquals("success", resultName);
}
/**
* Exercises the full wildcard resolution path through a real {@link com.opensymphony.xwork2.DefaultActionProxy}.
* <p>
* Config (from xwork-test-allowed-methods.xml):
* {@code <action name="Wild-*" class="HttpMethodsTestAction" method="{1}">}.
* URL {@code Wild-execute} resolves to {@code ActionSupport.execute()} no method-level
* HTTP annotation. {@code HttpMethodsTestAction} carries class-level
* {@code @AllowedHttpMethod(POST)}, so GET must be rejected end-to-end.
*/
public void testWildcardResolvedExecuteRejectsGetThroughRealProxy() throws Exception {
loadConfigurationProviders(new StrutsXmlConfigurationProvider(
"com/opensymphony/xwork2/config/providers/xwork-test-allowed-methods.xml"));
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/Wild-execute");
Map<String, Object> extraContext = ActionContext.of()
.withServletRequest(request)
.getContextMap();
ActionProxy proxy = actionProxyFactory.createActionProxy("", "Wild-execute", null, extraContext);
assertEquals("execute", proxy.getMethod());
assertTrue("Wildcard-resolved method must report isMethodSpecified()=true", proxy.isMethodSpecified());
HttpMethodInterceptor realInterceptor = new HttpMethodInterceptor();
String result = realInterceptor.intercept(proxy.getInvocation());
assertEquals("bad-request", result);
}
private void prepareActionInvocation(Object action) {
interceptor = new HttpMethodInterceptor();
invocation = new MockActionInvocation();
@@ -38,6 +38,7 @@ import com.opensymphony.xwork2.ognl.OgnlValueStack;
import com.opensymphony.xwork2.ognl.OgnlValueStackFactory;
import com.opensymphony.xwork2.ognl.accessor.CompoundRootAccessor;
import com.opensymphony.xwork2.ognl.accessor.RootAccessor;
import com.opensymphony.xwork2.util.Element;
import com.opensymphony.xwork2.util.ValueStack;
import com.opensymphony.xwork2.util.ValueStackFactory;
import com.opensymphony.xwork2.util.reflection.ReflectionContextState;
@@ -1012,6 +1013,44 @@ public class ParametersInterceptorTest extends XWorkTestCase {
container.inject(config.getInterceptors().get(0).getInterceptor());
}
/**
* WW-5700: a value that cannot be converted to the map's element type must not be stored.
* An unchecked s:checkbox with submitUnchecked="true" submits the CheckboxInterceptor's
* uncheckedValue, "false", which cannot become an Integer.
*/
public void testUnconvertibleValueIsNotBoundIntoTypedMap() {
CheckboxAction action = new CheckboxAction();
ValueStack vs = ActionContext.getContext().getValueStack();
vs.push(action);
ParametersInterceptor pi = new ParametersInterceptor();
container.inject(pi);
Map<String, Object> params = new HashMap<>();
params.put("capDeferral[100]", "1");
params.put("capDeferral[200]", "false");
pi.applyParameters(action, vs, HttpParameters.create(params).build());
Map<Long, Integer> capDeferral = action.getCapDeferral();
assertEquals("sanity: the convertible value must still bind", Integer.valueOf(1), capDeferral.get(100L));
for (Object entry : ((Map) capDeferral).entrySet()) {
Map.Entry e = (Map.Entry) entry;
assertTrue("key is not a Long: " + e.getKey(), e.getKey() instanceof Long);
assertTrue("value is not an Integer: " + e.getValue(), e.getValue() instanceof Integer);
}
}
public static class CheckboxAction {
@Element(value = Integer.class)
private final Map<Long, Integer> capDeferral = new HashMap<>();
@StrutsParameter(depth = 1)
public Map<Long, Integer> getCapDeferral() {
return capDeferral;
}
}
}
class ValidateAction implements ValidationAware {
@@ -146,5 +146,108 @@ public class PostbackResultTest extends StrutsInternalTestCase {
}
}
/**
* WW-5623: Verify that HTML special characters in finalLocation are properly
* escaped in the rendered form action attribute.
*/
public void testFormActionHtmlEscaping() throws Exception {
ActionContext context = ActionContext.getContext();
ValueStack stack = context.getValueStack();
MockHttpServletRequest req = new MockHttpServletRequest();
MockHttpServletResponse res = new MockHttpServletResponse();
context.put(ServletActionContext.HTTP_REQUEST, req);
context.put(ServletActionContext.HTTP_RESPONSE, res);
// Push an object with a malicious property onto the value stack
stack.push(new Object() {
public String getTargetUrl() {
return "/test\"onmouseover=\"alert(1)";
}
});
PostbackResult result = new PostbackResult();
result.setLocation("/redirect?url=${targetUrl}");
result.setPrependServletContext(false);
IMocksControl control = createControl();
ActionInvocation mockInvocation = control.createMock(ActionInvocation.class);
expect(mockInvocation.getInvocationContext()).andReturn(context).anyTimes();
expect(mockInvocation.getStack()).andReturn(stack).anyTimes();
control.replay();
result.setActionMapper(container.getInstance(ActionMapper.class));
// Call doExecute directly with a malicious location containing all critical chars
result.doExecute("/test\"onmouseover=\"alert(1)\"&param=<script>", mockInvocation);
String output = res.getContentAsString();
// The action attribute must contain escaped HTML entities
assertTrue("Double quote should be escaped to &quot;",
output.contains("action=\"/test&quot;onmouseover=&quot;alert(1)&quot;&amp;param=&lt;script&gt;\""));
// Must not contain unescaped double-quote that breaks out of the attribute
assertFalse("Raw double-quote must not appear in action value",
output.contains("action=\"/test\""));
control.verify();
}
/**
* WW-5623: Verify that each individual HTML special character is properly escaped.
*/
public void testFormActionEscapesAllHtmlSpecialChars() throws Exception {
ActionContext context = ActionContext.getContext();
MockHttpServletRequest req = new MockHttpServletRequest();
MockHttpServletResponse res = new MockHttpServletResponse();
context.put(ServletActionContext.HTTP_REQUEST, req);
context.put(ServletActionContext.HTTP_RESPONSE, res);
IMocksControl control = createControl();
ActionInvocation mockInvocation = control.createMock(ActionInvocation.class);
expect(mockInvocation.getInvocationContext()).andReturn(context).anyTimes();
control.replay();
PostbackResult result = new PostbackResult();
result.setActionMapper(container.getInstance(ActionMapper.class));
result.doExecute("/path?a=1&b=2\"<>", mockInvocation);
String output = res.getContentAsString();
assertTrue("Ampersand should be escaped", output.contains("&amp;"));
assertTrue("Double-quote should be escaped", output.contains("&quot;"));
assertTrue("Less-than should be escaped", output.contains("&lt;"));
assertTrue("Greater-than should be escaped", output.contains("&gt;"));
control.verify();
}
/**
* WW-5623: Verify that a clean location (no special chars) renders unchanged.
*/
public void testFormActionCleanLocationUnchanged() throws Exception {
ActionContext context = ActionContext.getContext();
MockHttpServletRequest req = new MockHttpServletRequest();
MockHttpServletResponse res = new MockHttpServletResponse();
context.put(ServletActionContext.HTTP_REQUEST, req);
context.put(ServletActionContext.HTTP_RESPONSE, res);
IMocksControl control = createControl();
ActionInvocation mockInvocation = control.createMock(ActionInvocation.class);
expect(mockInvocation.getInvocationContext()).andReturn(context).anyTimes();
control.replay();
PostbackResult result = new PostbackResult();
result.setActionMapper(container.getInstance(ActionMapper.class));
result.doExecute("/clean/path/action.do", mockInvocation);
String output = res.getContentAsString();
assertTrue("Clean location should render as-is in action attribute",
output.contains("action=\"/clean/path/action.do\""));
control.verify();
}
}
@@ -120,6 +120,16 @@ public class StreamResultTest extends StrutsInternalTestCase {
assertEquals("inline", response.getHeader("Content-disposition"));
}
public void testStreamResultWithNullCharSetExpression() throws Exception {
result.setParse(true);
result.setInputName("streamForImage");
result.setContentCharSet("${nullCharSetMethod}");
result.doExecute("helloworld", mai);
assertEquals("text/plain", response.getContentType());
}
public void testAllowCacheDefault() throws Exception {
result.setInputName("streamForImage");
@@ -310,6 +320,10 @@ public class StreamResultTest extends StrutsInternalTestCase {
public String getContentCharSetMethod() {
return "UTF-8";
}
public String getNullCharSetMethod() {
return null;
}
}
}

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