Compare commits

..

36 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
86 changed files with 2181 additions and 432 deletions
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
+1 -1
View File
@@ -24,7 +24,7 @@
<parent>
<groupId>org.apache.struts</groupId>
<artifactId>struts2-parent</artifactId>
<version>6.10.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.10.0</version>
<version>6.12.0-SNAPSHOT</version>
</parent>
<artifactId>struts2-rest-showcase</artifactId>
<packaging>war</packaging>
<version>6.10.0</version>
<version>6.12.0-SNAPSHOT</version>
<name>Struts 2 Rest Showcase Webapp</name>
<description>Struts 2 Rest Showcase Example</description>
+2 -2
View File
@@ -24,7 +24,7 @@
<parent>
<groupId>org.apache.struts</groupId>
<artifactId>struts2-apps</artifactId>
<version>6.10.0</version>
<version>6.12.0-SNAPSHOT</version>
</parent>
<artifactId>struts2-showcase</artifactId>
@@ -167,7 +167,7 @@
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-failsafe-plugin</artifactId>
<version>3.5.5</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.10.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 -3
View File
@@ -25,7 +25,7 @@
<parent>
<groupId>org.apache.struts</groupId>
<artifactId>struts2-parent</artifactId>
<version>6.10.0</version>
<version>6.12.0-SNAPSHOT</version>
</parent>
<artifactId>struts2-bom</artifactId>
@@ -43,7 +43,7 @@
</licenses>
<properties>
<struts-version.version>6.10.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>
@@ -189,7 +189,7 @@
</dependencyManagement>
<scm>
<tag>STRUTS_6_10_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.10.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.10.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.10.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.10.0</version>
<version>6.12.0-SNAPSHOT</version>
</parent>
<artifactId>struts2-core</artifactId>
<packaging>jar</packaging>
@@ -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());
}
@@ -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();
@@ -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());
@@ -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() {
@@ -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);
/**
@@ -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
@@ -518,6 +542,13 @@ 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}.
@@ -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;
@@ -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.
*
@@ -950,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) {
@@ -961,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();
@@ -970,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).
*
@@ -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)
*/
@@ -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
@@ -290,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
+2
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"
@@ -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,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;
}
}
}
@@ -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);
}
}
}
@@ -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();
@@ -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 {
@@ -0,0 +1,19 @@
#
# 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.
#
cache.static=Static cached value
Vendored
+146 -116
View File
@@ -19,7 +19,7 @@
# ----------------------------------------------------------------------------
# ----------------------------------------------------------------------------
# Apache Maven Wrapper startup batch script, version 3.2.0
# Apache Maven Wrapper startup batch script, version 3.3.4
#
# Required ENV vars:
# ------------------
@@ -33,75 +33,84 @@
# MAVEN_SKIP_RC - flag to disable loading of mavenrc files
# ----------------------------------------------------------------------------
if [ -z "$MAVEN_SKIP_RC" ] ; then
if [ -z "$MAVEN_SKIP_RC" ]; then
if [ -f /usr/local/etc/mavenrc ] ; then
if [ -f /usr/local/etc/mavenrc ]; then
. /usr/local/etc/mavenrc
fi
if [ -f /etc/mavenrc ] ; then
if [ -f /etc/mavenrc ]; then
. /etc/mavenrc
fi
if [ -f "$HOME/.mavenrc" ] ; then
if [ -f "$HOME/.mavenrc" ]; then
. "$HOME/.mavenrc"
fi
fi
# OS specific support. $var _must_ be set to either true or false.
cygwin=false;
darwin=false;
cygwin=false
darwin=false
mingw=false
case "$(uname)" in
CYGWIN*) cygwin=true ;;
MINGW*) mingw=true;;
Darwin*) darwin=true
# Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home
# See https://developer.apple.com/library/mac/qa/qa1170/_index.html
if [ -z "$JAVA_HOME" ]; then
if [ -x "/usr/libexec/java_home" ]; then
JAVA_HOME="$(/usr/libexec/java_home)"; export JAVA_HOME
else
JAVA_HOME="/Library/Java/Home"; export JAVA_HOME
fi
CYGWIN*) cygwin=true ;;
MINGW*) mingw=true ;;
Darwin*)
darwin=true
# Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home
# See https://developer.apple.com/library/mac/qa/qa1170/_index.html
if [ -z "$JAVA_HOME" ]; then
if [ -x "/usr/libexec/java_home" ]; then
JAVA_HOME="$(/usr/libexec/java_home)"
export JAVA_HOME
else
JAVA_HOME="/Library/Java/Home"
export JAVA_HOME
fi
;;
fi
;;
esac
if [ -z "$JAVA_HOME" ] ; then
if [ -r /etc/gentoo-release ] ; then
if [ -z "$JAVA_HOME" ]; then
if [ -r /etc/gentoo-release ]; then
JAVA_HOME=$(java-config --jre-home)
fi
fi
# For Cygwin, ensure paths are in UNIX format before anything is touched
if $cygwin ; then
[ -n "$JAVA_HOME" ] &&
JAVA_HOME=$(cygpath --unix "$JAVA_HOME")
[ -n "$CLASSPATH" ] &&
CLASSPATH=$(cygpath --path --unix "$CLASSPATH")
if $cygwin; then
[ -n "$JAVA_HOME" ] \
&& JAVA_HOME=$(cygpath --unix "$JAVA_HOME")
[ -n "$CLASSPATH" ] \
&& CLASSPATH=$(cygpath --path --unix "$CLASSPATH")
fi
# For Mingw, ensure paths are in UNIX format before anything is touched
if $mingw ; then
[ -n "$JAVA_HOME" ] && [ -d "$JAVA_HOME" ] &&
JAVA_HOME="$(cd "$JAVA_HOME" || (echo "cannot cd into $JAVA_HOME."; exit 1); pwd)"
if $mingw; then
[ -n "$JAVA_HOME" ] && [ -d "$JAVA_HOME" ] \
&& JAVA_HOME="$(
cd "$JAVA_HOME" || (
echo "cannot cd into $JAVA_HOME." >&2
exit 1
)
pwd
)"
fi
if [ -z "$JAVA_HOME" ]; then
javaExecutable="$(which javac)"
if [ -n "$javaExecutable" ] && ! [ "$(expr "\"$javaExecutable\"" : '\([^ ]*\)')" = "no" ]; then
if [ -n "$javaExecutable" ] && ! [ "$(expr "$javaExecutable" : '\([^ ]*\)')" = "no" ]; then
# readlink(1) is not available as standard on Solaris 10.
readLink=$(which readlink)
if [ ! "$(expr "$readLink" : '\([^ ]*\)')" = "no" ]; then
if $darwin ; then
javaHome="$(dirname "\"$javaExecutable\"")"
javaExecutable="$(cd "\"$javaHome\"" && pwd -P)/javac"
if $darwin; then
javaHome="$(dirname "$javaExecutable")"
javaExecutable="$(cd "$javaHome" && pwd -P)/javac"
else
javaExecutable="$(readlink -f "\"$javaExecutable\"")"
javaExecutable="$(readlink -f "$javaExecutable")"
fi
javaHome="$(dirname "\"$javaExecutable\"")"
javaHome="$(dirname "$javaExecutable")"
javaHome=$(expr "$javaHome" : '\(.*\)/bin')
JAVA_HOME="$javaHome"
export JAVA_HOME
@@ -109,52 +118,60 @@ if [ -z "$JAVA_HOME" ]; then
fi
fi
if [ -z "$JAVACMD" ] ; then
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
if [ -z "$JAVACMD" ]; then
if [ -n "$JAVA_HOME" ]; then
if [ -x "$JAVA_HOME/jre/sh/java" ]; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD="$JAVA_HOME/jre/sh/java"
else
JAVACMD="$JAVA_HOME/bin/java"
fi
else
JAVACMD="$(\unset -f command 2>/dev/null; \command -v java)"
JAVACMD="$(
\unset -f command 2>/dev/null
\command -v java
)"
fi
fi
if [ ! -x "$JAVACMD" ] ; then
if [ ! -x "$JAVACMD" ]; then
echo "Error: JAVA_HOME is not defined correctly." >&2
echo " We cannot execute $JAVACMD" >&2
exit 1
fi
if [ -z "$JAVA_HOME" ] ; then
echo "Warning: JAVA_HOME environment variable is not set."
if [ -z "$JAVA_HOME" ]; then
echo "Warning: JAVA_HOME environment variable is not set." >&2
fi
# traverses directory structure from process work directory to filesystem root
# first directory with .mvn subdirectory is considered project base directory
find_maven_basedir() {
if [ -z "$1" ]
then
echo "Path not specified to find_maven_basedir"
if [ -z "$1" ]; then
echo "Path not specified to find_maven_basedir" >&2
return 1
fi
basedir="$1"
wdir="$1"
while [ "$wdir" != '/' ] ; do
if [ -d "$wdir"/.mvn ] ; then
while [ "$wdir" != '/' ]; do
if [ -d "$wdir"/.mvn ]; then
basedir=$wdir
break
fi
# workaround for JBEAP-8937 (on Solaris 10/Sparc)
if [ -d "${wdir}" ]; then
wdir=$(cd "$wdir/.." || exit 1; pwd)
wdir=$(
cd "$wdir/.." || exit 1
pwd
)
fi
# end of workaround
done
printf '%s' "$(cd "$basedir" || exit 1; pwd)"
printf '%s' "$(
cd "$basedir" || exit 1
pwd
)"
}
# concatenates all lines of a file
@@ -165,7 +182,7 @@ concat_lines() {
# enabled. Otherwise, we may read lines that are delimited with
# \r\n and produce $'-Xarg\r' rather than -Xarg due to word
# splitting rules.
tr -s '\r\n' ' ' < "$1"
tr -s '\r\n' ' ' <"$1"
fi
}
@@ -177,75 +194,85 @@ log() {
BASE_DIR=$(find_maven_basedir "$(dirname "$0")")
if [ -z "$BASE_DIR" ]; then
exit 1;
exit 1
fi
MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"}; export MAVEN_PROJECTBASEDIR
MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"}
export MAVEN_PROJECTBASEDIR
log "$MAVEN_PROJECTBASEDIR"
trim() {
# MWRAPPER-139:
# Trims trailing and leading whitespace, carriage returns, tabs, and linefeeds.
# Needed for removing poorly interpreted newline sequences when running in more
# exotic environments such as mingw bash on Windows.
printf "%s" "${1}" | tr -d '[:space:]'
}
##########################################################################################
# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central
# This allows using the maven wrapper in projects that prohibit checking in binary data.
##########################################################################################
wrapperJarPath="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar"
if [ -r "$wrapperJarPath" ]; then
log "Found $wrapperJarPath"
log "Found $wrapperJarPath"
else
log "Couldn't find $wrapperJarPath, downloading it ..."
log "Couldn't find $wrapperJarPath, downloading it ..."
if [ -n "$MVNW_REPOURL" ]; then
wrapperUrl="$MVNW_REPOURL/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar"
if [ -n "$MVNW_REPOURL" ]; then
wrapperUrl="$MVNW_REPOURL/org/apache/maven/wrapper/maven-wrapper/3.3.4/maven-wrapper-3.3.4.jar"
else
wrapperUrl="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.3.4/maven-wrapper-3.3.4.jar"
fi
while IFS="=" read -r key value; do
case "$key" in wrapperUrl)
wrapperUrl=$(trim "${value-}")
break
;;
esac
done <"$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties"
log "Downloading from: $wrapperUrl"
if $cygwin; then
wrapperJarPath=$(cygpath --path --windows "$wrapperJarPath")
fi
if command -v wget >/dev/null; then
log "Found wget ... using wget"
[ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--quiet"
if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then
wget ${QUIET:+"$QUIET"} "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath"
else
wrapperUrl="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar"
wget ${QUIET:+"$QUIET"} --http-user="$MVNW_USERNAME" --http-password="$MVNW_PASSWORD" "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath"
fi
while IFS="=" read -r key value; do
# Remove '\r' from value to allow usage on windows as IFS does not consider '\r' as a separator ( considers space, tab, new line ('\n'), and custom '=' )
safeValue=$(echo "$value" | tr -d '\r')
case "$key" in (wrapperUrl) wrapperUrl="$safeValue"; break ;;
esac
done < "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties"
log "Downloading from: $wrapperUrl"
elif command -v curl >/dev/null; then
log "Found curl ... using curl"
[ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--silent"
if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then
curl ${QUIET:+"$QUIET"} -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath"
else
curl ${QUIET:+"$QUIET"} --user "$MVNW_USERNAME:$MVNW_PASSWORD" -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath"
fi
else
log "Falling back to using Java to download"
javaSource="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.java"
javaClass="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.class"
# For Cygwin, switch paths to Windows format before running javac
if $cygwin; then
wrapperJarPath=$(cygpath --path --windows "$wrapperJarPath")
javaSource=$(cygpath --path --windows "$javaSource")
javaClass=$(cygpath --path --windows "$javaClass")
fi
if command -v wget > /dev/null; then
log "Found wget ... using wget"
[ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--quiet"
if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then
wget $QUIET "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath"
else
wget $QUIET --http-user="$MVNW_USERNAME" --http-password="$MVNW_PASSWORD" "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath"
fi
elif command -v curl > /dev/null; then
log "Found curl ... using curl"
[ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--silent"
if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then
curl $QUIET -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath"
else
curl $QUIET --user "$MVNW_USERNAME:$MVNW_PASSWORD" -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath"
fi
else
log "Falling back to using Java to download"
javaSource="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.java"
javaClass="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.class"
# For Cygwin, switch paths to Windows format before running javac
if $cygwin; then
javaSource=$(cygpath --path --windows "$javaSource")
javaClass=$(cygpath --path --windows "$javaClass")
fi
if [ -e "$javaSource" ]; then
if [ ! -e "$javaClass" ]; then
log " - Compiling MavenWrapperDownloader.java ..."
("$JAVA_HOME/bin/javac" "$javaSource")
fi
if [ -e "$javaClass" ]; then
log " - Running MavenWrapperDownloader.java ..."
("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$wrapperUrl" "$wrapperJarPath") || rm -f "$wrapperJarPath"
fi
fi
if [ -e "$javaSource" ]; then
if [ ! -e "$javaClass" ]; then
log " - Compiling MavenWrapperDownloader.java ..."
("$JAVA_HOME/bin/javac" "$javaSource")
fi
if [ -e "$javaClass" ]; then
log " - Running MavenWrapperDownloader.java ..."
("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$wrapperUrl" "$wrapperJarPath") || rm -f "$wrapperJarPath"
fi
fi
fi
fi
##########################################################################################
# End of extension
@@ -254,22 +281,25 @@ fi
# If specified, validate the SHA-256 sum of the Maven wrapper jar file
wrapperSha256Sum=""
while IFS="=" read -r key value; do
case "$key" in (wrapperSha256Sum) wrapperSha256Sum=$value; break ;;
case "$key" in wrapperSha256Sum)
wrapperSha256Sum=$(trim "${value-}")
break
;;
esac
done < "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties"
done <"$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties"
if [ -n "$wrapperSha256Sum" ]; then
wrapperSha256Result=false
if command -v sha256sum > /dev/null; then
if echo "$wrapperSha256Sum $wrapperJarPath" | sha256sum -c > /dev/null 2>&1; then
if command -v sha256sum >/dev/null; then
if echo "$wrapperSha256Sum $wrapperJarPath" | sha256sum -c - >/dev/null 2>&1; then
wrapperSha256Result=true
fi
elif command -v shasum > /dev/null; then
if echo "$wrapperSha256Sum $wrapperJarPath" | shasum -a 256 -c > /dev/null 2>&1; then
elif command -v shasum >/dev/null; then
if echo "$wrapperSha256Sum $wrapperJarPath" | shasum -a 256 -c >/dev/null 2>&1; then
wrapperSha256Result=true
fi
else
echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available."
echo "Please install either command, or disable validation by removing 'wrapperSha256Sum' from your maven-wrapper.properties."
echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." >&2
echo "Please install either command, or disable validation by removing 'wrapperSha256Sum' from your maven-wrapper.properties." >&2
exit 1
fi
if [ $wrapperSha256Result = false ]; then
@@ -284,12 +314,12 @@ MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS"
# For Cygwin, switch paths to Windows format before running java
if $cygwin; then
[ -n "$JAVA_HOME" ] &&
JAVA_HOME=$(cygpath --path --windows "$JAVA_HOME")
[ -n "$CLASSPATH" ] &&
CLASSPATH=$(cygpath --path --windows "$CLASSPATH")
[ -n "$MAVEN_PROJECTBASEDIR" ] &&
MAVEN_PROJECTBASEDIR=$(cygpath --path --windows "$MAVEN_PROJECTBASEDIR")
[ -n "$JAVA_HOME" ] \
&& JAVA_HOME=$(cygpath --path --windows "$JAVA_HOME")
[ -n "$CLASSPATH" ] \
&& CLASSPATH=$(cygpath --path --windows "$CLASSPATH")
[ -n "$MAVEN_PROJECTBASEDIR" ] \
&& MAVEN_PROJECTBASEDIR=$(cygpath --path --windows "$MAVEN_PROJECTBASEDIR")
fi
# Provide a "standardized" way to retrieve the CLI args that will
Vendored
+206 -205
View File
@@ -1,205 +1,206 @@
@REM ----------------------------------------------------------------------------
@REM Licensed to the Apache Software Foundation (ASF) under one
@REM or more contributor license agreements. See the NOTICE file
@REM distributed with this work for additional information
@REM regarding copyright ownership. The ASF licenses this file
@REM to you under the Apache License, Version 2.0 (the
@REM "License"); you may not use this file except in compliance
@REM with the License. You may obtain a copy of the License at
@REM
@REM http://www.apache.org/licenses/LICENSE-2.0
@REM
@REM Unless required by applicable law or agreed to in writing,
@REM software distributed under the License is distributed on an
@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
@REM KIND, either express or implied. See the License for the
@REM specific language governing permissions and limitations
@REM under the License.
@REM ----------------------------------------------------------------------------
@REM ----------------------------------------------------------------------------
@REM Apache Maven Wrapper startup batch script, version 3.2.0
@REM
@REM Required ENV vars:
@REM JAVA_HOME - location of a JDK home dir
@REM
@REM Optional ENV vars
@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands
@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending
@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven
@REM e.g. to debug Maven itself, use
@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000
@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files
@REM ----------------------------------------------------------------------------
@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on'
@echo off
@REM set title of command window
title %0
@REM enable echoing by setting MAVEN_BATCH_ECHO to 'on'
@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO%
@REM set %HOME% to equivalent of $HOME
if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%")
@REM Execute a user defined script before this one
if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre
@REM check for pre script, once with legacy .bat ending and once with .cmd ending
if exist "%USERPROFILE%\mavenrc_pre.bat" call "%USERPROFILE%\mavenrc_pre.bat" %*
if exist "%USERPROFILE%\mavenrc_pre.cmd" call "%USERPROFILE%\mavenrc_pre.cmd" %*
:skipRcPre
@setlocal
set ERROR_CODE=0
@REM To isolate internal variables from possible post scripts, we use another setlocal
@setlocal
@REM ==== START VALIDATION ====
if not "%JAVA_HOME%" == "" goto OkJHome
echo.
echo Error: JAVA_HOME not found in your environment. >&2
echo Please set the JAVA_HOME variable in your environment to match the >&2
echo location of your Java installation. >&2
echo.
goto error
:OkJHome
if exist "%JAVA_HOME%\bin\java.exe" goto init
echo.
echo Error: JAVA_HOME is set to an invalid directory. >&2
echo JAVA_HOME = "%JAVA_HOME%" >&2
echo Please set the JAVA_HOME variable in your environment to match the >&2
echo location of your Java installation. >&2
echo.
goto error
@REM ==== END VALIDATION ====
:init
@REM Find the project base dir, i.e. the directory that contains the folder ".mvn".
@REM Fallback to current working directory if not found.
set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR%
IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir
set EXEC_DIR=%CD%
set WDIR=%EXEC_DIR%
:findBaseDir
IF EXIST "%WDIR%"\.mvn goto baseDirFound
cd ..
IF "%WDIR%"=="%CD%" goto baseDirNotFound
set WDIR=%CD%
goto findBaseDir
:baseDirFound
set MAVEN_PROJECTBASEDIR=%WDIR%
cd "%EXEC_DIR%"
goto endDetectBaseDir
:baseDirNotFound
set MAVEN_PROJECTBASEDIR=%EXEC_DIR%
cd "%EXEC_DIR%"
:endDetectBaseDir
IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig
@setlocal EnableExtensions EnableDelayedExpansion
for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a
@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS%
:endReadAdditionalConfig
SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe"
set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar"
set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain
set WRAPPER_URL="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar"
FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO (
IF "%%A"=="wrapperUrl" SET WRAPPER_URL=%%B
)
@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central
@REM This allows using the maven wrapper in projects that prohibit checking in binary data.
if exist %WRAPPER_JAR% (
if "%MVNW_VERBOSE%" == "true" (
echo Found %WRAPPER_JAR%
)
) else (
if not "%MVNW_REPOURL%" == "" (
SET WRAPPER_URL="%MVNW_REPOURL%/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar"
)
if "%MVNW_VERBOSE%" == "true" (
echo Couldn't find %WRAPPER_JAR%, downloading it ...
echo Downloading from: %WRAPPER_URL%
)
powershell -Command "&{"^
"$webclient = new-object System.Net.WebClient;"^
"if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^
"$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^
"}"^
"[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%WRAPPER_URL%', '%WRAPPER_JAR%')"^
"}"
if "%MVNW_VERBOSE%" == "true" (
echo Finished downloading %WRAPPER_JAR%
)
)
@REM End of extension
@REM If specified, validate the SHA-256 sum of the Maven wrapper jar file
SET WRAPPER_SHA_256_SUM=""
FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO (
IF "%%A"=="wrapperSha256Sum" SET WRAPPER_SHA_256_SUM=%%B
)
IF NOT %WRAPPER_SHA_256_SUM%=="" (
powershell -Command "&{"^
"$hash = (Get-FileHash \"%WRAPPER_JAR%\" -Algorithm SHA256).Hash.ToLower();"^
"If('%WRAPPER_SHA_256_SUM%' -ne $hash){"^
" Write-Output 'Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised.';"^
" Write-Output 'Investigate or delete %WRAPPER_JAR% to attempt a clean download.';"^
" Write-Output 'If you updated your Maven version, you need to update the specified wrapperSha256Sum property.';"^
" exit 1;"^
"}"^
"}"
if ERRORLEVEL 1 goto error
)
@REM Provide a "standardized" way to retrieve the CLI args that will
@REM work with both Windows and non-Windows executions.
set MAVEN_CMD_LINE_ARGS=%*
%MAVEN_JAVA_EXE% ^
%JVM_CONFIG_MAVEN_PROPS% ^
%MAVEN_OPTS% ^
%MAVEN_DEBUG_OPTS% ^
-classpath %WRAPPER_JAR% ^
"-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" ^
%WRAPPER_LAUNCHER% %MAVEN_CONFIG% %*
if ERRORLEVEL 1 goto error
goto end
:error
set ERROR_CODE=1
:end
@endlocal & set ERROR_CODE=%ERROR_CODE%
if not "%MAVEN_SKIP_RC%"=="" goto skipRcPost
@REM check for post script, once with legacy .bat ending and once with .cmd ending
if exist "%USERPROFILE%\mavenrc_post.bat" call "%USERPROFILE%\mavenrc_post.bat"
if exist "%USERPROFILE%\mavenrc_post.cmd" call "%USERPROFILE%\mavenrc_post.cmd"
:skipRcPost
@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on'
if "%MAVEN_BATCH_PAUSE%"=="on" pause
if "%MAVEN_TERMINATE_CMD%"=="on" exit %ERROR_CODE%
cmd /C exit /B %ERROR_CODE%
@REM ----------------------------------------------------------------------------
@REM Licensed to the Apache Software Foundation (ASF) under one
@REM or more contributor license agreements. See the NOTICE file
@REM distributed with this work for additional information
@REM regarding copyright ownership. The ASF licenses this file
@REM to you under the Apache License, Version 2.0 (the
@REM "License"); you may not use this file except in compliance
@REM with the License. You may obtain a copy of the License at
@REM
@REM http://www.apache.org/licenses/LICENSE-2.0
@REM
@REM Unless required by applicable law or agreed to in writing,
@REM software distributed under the License is distributed on an
@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
@REM KIND, either express or implied. See the License for the
@REM specific language governing permissions and limitations
@REM under the License.
@REM ----------------------------------------------------------------------------
@REM ----------------------------------------------------------------------------
@REM Apache Maven Wrapper startup batch script, version 3.3.4
@REM
@REM Required ENV vars:
@REM JAVA_HOME - location of a JDK home dir
@REM
@REM Optional ENV vars
@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands
@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending
@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven
@REM e.g. to debug Maven itself, use
@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000
@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files
@REM ----------------------------------------------------------------------------
@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on'
@echo off
@REM set title of command window
title %0
@REM enable echoing by setting MAVEN_BATCH_ECHO to 'on'
@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO%
@REM set %HOME% to equivalent of $HOME
if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%")
@REM Execute a user defined script before this one
if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre
@REM check for pre script, once with legacy .bat ending and once with .cmd ending
if exist "%USERPROFILE%\mavenrc_pre.bat" call "%USERPROFILE%\mavenrc_pre.bat" %*
if exist "%USERPROFILE%\mavenrc_pre.cmd" call "%USERPROFILE%\mavenrc_pre.cmd" %*
:skipRcPre
@setlocal
set ERROR_CODE=0
@REM To isolate internal variables from possible post scripts, we use another setlocal
@setlocal
@REM ==== START VALIDATION ====
if not "%JAVA_HOME%" == "" goto OkJHome
echo. >&2
echo Error: JAVA_HOME not found in your environment. >&2
echo Please set the JAVA_HOME variable in your environment to match the >&2
echo location of your Java installation. >&2
echo. >&2
goto error
:OkJHome
if exist "%JAVA_HOME%\bin\java.exe" goto init
echo. >&2
echo Error: JAVA_HOME is set to an invalid directory. >&2
echo JAVA_HOME = "%JAVA_HOME%" >&2
echo Please set the JAVA_HOME variable in your environment to match the >&2
echo location of your Java installation. >&2
echo. >&2
goto error
@REM ==== END VALIDATION ====
:init
@REM Find the project base dir, i.e. the directory that contains the folder ".mvn".
@REM Fallback to current working directory if not found.
set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR%
IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir
set EXEC_DIR=%CD%
set WDIR=%EXEC_DIR%
:findBaseDir
IF EXIST "%WDIR%"\.mvn goto baseDirFound
cd ..
IF "%WDIR%"=="%CD%" goto baseDirNotFound
set WDIR=%CD%
goto findBaseDir
:baseDirFound
set MAVEN_PROJECTBASEDIR=%WDIR%
cd "%EXEC_DIR%"
goto endDetectBaseDir
:baseDirNotFound
set MAVEN_PROJECTBASEDIR=%EXEC_DIR%
cd "%EXEC_DIR%"
:endDetectBaseDir
IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig
@setlocal EnableExtensions EnableDelayedExpansion
for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a
@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS%
:endReadAdditionalConfig
SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe"
set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar"
set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain
set WRAPPER_URL="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.3.4/maven-wrapper-3.3.4.jar"
FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO (
IF "%%A"=="wrapperUrl" SET WRAPPER_URL=%%B
)
@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central
@REM This allows using the maven wrapper in projects that prohibit checking in binary data.
if exist %WRAPPER_JAR% (
if "%MVNW_VERBOSE%" == "true" (
echo Found %WRAPPER_JAR%
)
) else (
if not "%MVNW_REPOURL%" == "" (
SET WRAPPER_URL="%MVNW_REPOURL%/org/apache/maven/wrapper/maven-wrapper/3.3.4/maven-wrapper-3.3.4.jar"
)
if "%MVNW_VERBOSE%" == "true" (
echo Couldn't find %WRAPPER_JAR%, downloading it ...
echo Downloading from: %WRAPPER_URL%
)
powershell -Command "&{"^
"$webclient = new-object System.Net.WebClient;"^
"if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^
"$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^
"}"^
"[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%WRAPPER_URL%', '%WRAPPER_JAR%')"^
"}"
if "%MVNW_VERBOSE%" == "true" (
echo Finished downloading %WRAPPER_JAR%
)
)
@REM End of extension
@REM If specified, validate the SHA-256 sum of the Maven wrapper jar file
SET WRAPPER_SHA_256_SUM=""
FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO (
IF "%%A"=="wrapperSha256Sum" SET WRAPPER_SHA_256_SUM=%%B
)
IF NOT %WRAPPER_SHA_256_SUM%=="" (
powershell -Command "&{"^
"Import-Module $PSHOME\Modules\Microsoft.PowerShell.Utility -Function Get-FileHash;"^
"$hash = (Get-FileHash \"%WRAPPER_JAR%\" -Algorithm SHA256).Hash.ToLower();"^
"If('%WRAPPER_SHA_256_SUM%' -ne $hash){"^
" Write-Error 'Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised.';"^
" Write-Error 'Investigate or delete %WRAPPER_JAR% to attempt a clean download.';"^
" Write-Error 'If you updated your Maven version, you need to update the specified wrapperSha256Sum property.';"^
" exit 1;"^
"}"^
"}"
if ERRORLEVEL 1 goto error
)
@REM Provide a "standardized" way to retrieve the CLI args that will
@REM work with both Windows and non-Windows executions.
set MAVEN_CMD_LINE_ARGS=%*
%MAVEN_JAVA_EXE% ^
%JVM_CONFIG_MAVEN_PROPS% ^
%MAVEN_OPTS% ^
%MAVEN_DEBUG_OPTS% ^
-classpath %WRAPPER_JAR% ^
"-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" ^
%WRAPPER_LAUNCHER% %MAVEN_CONFIG% %*
if ERRORLEVEL 1 goto error
goto end
:error
set ERROR_CODE=1
:end
@endlocal & set ERROR_CODE=%ERROR_CODE%
if not "%MAVEN_SKIP_RC%"=="" goto skipRcPost
@REM check for post script, once with legacy .bat ending and once with .cmd ending
if exist "%USERPROFILE%\mavenrc_post.bat" call "%USERPROFILE%\mavenrc_post.bat"
if exist "%USERPROFILE%\mavenrc_post.cmd" call "%USERPROFILE%\mavenrc_post.cmd"
:skipRcPost
@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on'
if "%MAVEN_BATCH_PAUSE%"=="on" pause
if "%MAVEN_TERMINATE_CMD%"=="on" exit %ERROR_CODE%
cmd /C exit /B %ERROR_CODE%
+1 -1
View File
@@ -24,7 +24,7 @@
<parent>
<groupId>org.apache.struts</groupId>
<artifactId>struts2-plugins</artifactId>
<version>6.10.0</version>
<version>6.12.0-SNAPSHOT</version>
</parent>
<artifactId>struts2-async-plugin</artifactId>
+1 -1
View File
@@ -24,7 +24,7 @@
<parent>
<groupId>org.apache.struts</groupId>
<artifactId>struts2-plugins</artifactId>
<version>6.10.0</version>
<version>6.12.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
+1 -1
View File
@@ -25,7 +25,7 @@
<parent>
<groupId>org.apache.struts</groupId>
<artifactId>struts2-plugins</artifactId>
<version>6.10.0</version>
<version>6.12.0-SNAPSHOT</version>
</parent>
<artifactId>struts2-cdi-plugin</artifactId>
+1 -1
View File
@@ -24,7 +24,7 @@
<parent>
<groupId>org.apache.struts</groupId>
<artifactId>struts2-plugins</artifactId>
<version>6.10.0</version>
<version>6.12.0-SNAPSHOT</version>
</parent>
<artifactId>struts2-config-browser-plugin</artifactId>
+1 -1
View File
@@ -24,7 +24,7 @@
<parent>
<groupId>org.apache.struts</groupId>
<artifactId>struts2-plugins</artifactId>
<version>6.10.0</version>
<version>6.12.0-SNAPSHOT</version>
</parent>
<artifactId>struts2-convention-plugin</artifactId>
+1 -1
View File
@@ -24,7 +24,7 @@
<parent>
<groupId>org.apache.struts</groupId>
<artifactId>struts2-plugins</artifactId>
<version>6.10.0</version>
<version>6.12.0-SNAPSHOT</version>
</parent>
<artifactId>struts2-dwr-plugin</artifactId>
+1 -1
View File
@@ -24,7 +24,7 @@
<parent>
<groupId>org.apache.struts</groupId>
<artifactId>struts2-plugins</artifactId>
<version>6.10.0</version>
<version>6.12.0-SNAPSHOT</version>
</parent>
<artifactId>struts2-embeddedjsp-plugin</artifactId>
+1 -1
View File
@@ -24,7 +24,7 @@
<parent>
<groupId>org.apache.struts</groupId>
<artifactId>struts2-plugins</artifactId>
<version>6.10.0</version>
<version>6.12.0-SNAPSHOT</version>
</parent>
<artifactId>struts2-gxp-plugin</artifactId>
+1 -1
View File
@@ -24,7 +24,7 @@
<parent>
<groupId>org.apache.struts</groupId>
<artifactId>struts2-plugins</artifactId>
<version>6.10.0</version>
<version>6.12.0-SNAPSHOT</version>
</parent>
<artifactId>struts2-jasperreports-plugin</artifactId>
+1 -1
View File
@@ -25,7 +25,7 @@
<parent>
<groupId>org.apache.struts</groupId>
<artifactId>struts2-plugins</artifactId>
<version>6.10.0</version>
<version>6.12.0-SNAPSHOT</version>
</parent>
<artifactId>struts2-javatemplates-plugin</artifactId>
+1 -1
View File
@@ -24,7 +24,7 @@
<parent>
<groupId>org.apache.struts</groupId>
<artifactId>struts2-plugins</artifactId>
<version>6.10.0</version>
<version>6.12.0-SNAPSHOT</version>
</parent>
<artifactId>struts2-jfreechart-plugin</artifactId>
+1 -1
View File
@@ -24,7 +24,7 @@
<parent>
<groupId>org.apache.struts</groupId>
<artifactId>struts2-plugins</artifactId>
<version>6.10.0</version>
<version>6.12.0-SNAPSHOT</version>
</parent>
<artifactId>struts2-json-plugin</artifactId>
@@ -57,6 +57,9 @@ public class JSONUtil {
public final static String RFC3339_FORMAT = "yyyy-MM-dd'T'HH:mm:ss";
public static final boolean CACHE_BEAN_INFO_DEFAULT = true;
/** Chunk size used to read input incrementally while applying the length limit. */
private static final int READ_CHUNK_SIZE = 8192;
private static final Logger LOG = LogManager.getLogger(JSONUtil.class);
@@ -337,13 +340,15 @@ public class JSONUtil {
*/
public Object deserializeInput(Reader reader, int maxLength, int maxElements, int maxDepth,
int maxStringLength, int maxKeyLength) throws JSONException {
BufferedReader bufferReader = new BufferedReader(reader);
StringBuilder buffer = new StringBuilder();
String line;
char[] chunk = new char[READ_CHUNK_SIZE];
try {
while ((line = bufferReader.readLine()) != null) {
buffer.append(line);
int read;
// Apply the limit while reading rather than afterwards, so input that contains no
// line terminator is not accumulated in full before the limit can be evaluated.
while ((read = reader.read(chunk)) != -1) {
buffer.append(chunk, 0, read);
if (buffer.length() > maxLength) {
throw new JSONException("JSON input length exceeds maximum allowed length of " + maxLength);
}
@@ -0,0 +1,119 @@
/*
* 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.json;
import org.junit.Test;
import java.io.Reader;
import java.io.StringReader;
import java.util.Map;
import java.util.concurrent.atomic.AtomicLong;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
/**
* Verifies that {@link JSONUtil#deserializeInput(Reader, int, int, int, int, int)} applies the
* configured input length limit while reading, bounding how much input is consumed before the limit
* takes effect, and that input within the limit still parses.
*/
public class JSONUtilInputLimitTest {
/**
* Emits {@code total} characters with no line terminator anywhere, and records how many
* characters the caller actually consumed.
*/
private static final class UnterminatedReader extends Reader {
private final long total;
private final AtomicLong consumed;
private long produced = 0;
UnterminatedReader(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
}
}
@Test
public void inputWithoutLineTerminatorIsLimitedWhileReading() {
int maxLength = 1024;
long inputSize = 64L * 1024 * 1024;
AtomicLong consumed = new AtomicLong();
JSONUtil util = new JSONUtil();
Reader input = new UnterminatedReader(inputSize, consumed);
try {
util.deserializeInput(input, maxLength, 100, 10, 1000, 100);
fail("Expected JSONException for exceeding max length");
} catch (JSONException expected) {
// the limit is expected to be reported
}
long read = consumed.get();
// Reading proceeds in chunks, so a single chunk of overshoot beyond the limit is expected.
assertTrue("Consumed " + read + " characters for a limit of " + maxLength,
read < maxLength + 65_536L);
}
@Test
public void inputWithinLimitIsParsed() throws JSONException {
JSONUtil util = new JSONUtil();
Object result = util.deserializeInput(
new StringReader("{\"a\":1, \"b\":\"hello\"}"), 1024, 100, 10, 1000, 100);
assertTrue("Expected a parsed JSON object", result instanceof Map);
assertEquals(1L, ((Map<?, ?>) result).get("a"));
assertEquals("hello", ((Map<?, ?>) result).get("b"));
}
@Test
public void inputSpanningMultipleLinesIsParsed() throws JSONException {
JSONUtil util = new JSONUtil();
// Line terminators between tokens are insignificant whitespace to the reader.
Object result = util.deserializeInput(
new StringReader("{\n\"a\":1,\n\"b\":2\n}"), 1024, 100, 10, 1000, 100);
assertTrue("Expected a parsed JSON object", result instanceof Map);
assertEquals(1L, ((Map<?, ?>) result).get("a"));
assertEquals(2L, ((Map<?, ?>) result).get("b"));
}
}
+1 -1
View File
@@ -24,7 +24,7 @@
<parent>
<groupId>org.apache.struts</groupId>
<artifactId>struts2-plugins</artifactId>
<version>6.10.0</version>
<version>6.12.0-SNAPSHOT</version>
</parent>
<artifactId>struts2-junit-plugin</artifactId>
+1 -1
View File
@@ -24,7 +24,7 @@
<parent>
<groupId>org.apache.struts</groupId>
<artifactId>struts2-plugins</artifactId>
<version>6.10.0</version>
<version>6.12.0-SNAPSHOT</version>
</parent>
<artifactId>struts2-osgi-plugin</artifactId>
+1 -1
View File
@@ -24,7 +24,7 @@
<parent>
<groupId>org.apache.struts</groupId>
<artifactId>struts2-plugins</artifactId>
<version>6.10.0</version>
<version>6.12.0-SNAPSHOT</version>
</parent>
<artifactId>struts2-oval-plugin</artifactId>
+1 -1
View File
@@ -24,7 +24,7 @@
<parent>
<groupId>org.apache.struts</groupId>
<artifactId>struts2-plugins</artifactId>
<version>6.10.0</version>
<version>6.12.0-SNAPSHOT</version>
</parent>
<artifactId>struts2-pell-multipart-plugin</artifactId>
+1 -1
View File
@@ -24,7 +24,7 @@
<parent>
<groupId>org.apache.struts</groupId>
<artifactId>struts2-plugins</artifactId>
<version>6.10.0</version>
<version>6.12.0-SNAPSHOT</version>
</parent>
<artifactId>struts2-plexus-plugin</artifactId>
+1 -1
View File
@@ -24,7 +24,7 @@
<parent>
<groupId>org.apache.struts</groupId>
<artifactId>struts2-parent</artifactId>
<version>6.10.0</version>
<version>6.12.0-SNAPSHOT</version>
</parent>
<artifactId>struts2-plugins</artifactId>
+1 -1
View File
@@ -24,7 +24,7 @@
<parent>
<groupId>org.apache.struts</groupId>
<artifactId>struts2-plugins</artifactId>
<version>6.10.0</version>
<version>6.12.0-SNAPSHOT</version>
</parent>
<artifactId>struts2-portlet-junit-plugin</artifactId>
+1 -1
View File
@@ -24,7 +24,7 @@
<parent>
<groupId>org.apache.struts</groupId>
<artifactId>struts2-plugins</artifactId>
<version>6.10.0</version>
<version>6.12.0-SNAPSHOT</version>
</parent>
<artifactId>struts2-portlet-mocks-plugin</artifactId>
+1 -1
View File
@@ -24,7 +24,7 @@
<parent>
<groupId>org.apache.struts</groupId>
<artifactId>struts2-plugins</artifactId>
<version>6.10.0</version>
<version>6.12.0-SNAPSHOT</version>
</parent>
<artifactId>struts2-portlet-tiles-plugin</artifactId>
+1 -1
View File
@@ -24,7 +24,7 @@
<parent>
<groupId>org.apache.struts</groupId>
<artifactId>struts2-plugins</artifactId>
<version>6.10.0</version>
<version>6.12.0-SNAPSHOT</version>
</parent>
<artifactId>struts2-portlet-plugin</artifactId>
+1 -1
View File
@@ -24,7 +24,7 @@
<parent>
<groupId>org.apache.struts</groupId>
<artifactId>struts2-plugins</artifactId>
<version>6.10.0</version>
<version>6.12.0-SNAPSHOT</version>
</parent>
<artifactId>struts2-rest-plugin</artifactId>
@@ -351,6 +351,7 @@ public class RestActionMapper extends DefaultActionMapper {
Configuration config = configManager.getConfiguration();
String prefix = uri.substring(0, lastSlash);
namespace = "";
boolean rootAvailable = false;
// Find the longest matching namespace, defaulting to the default
for (Object o : config.getPackageConfigs().values()) {
String ns = ((PackageConfig) o).getNamespace();
@@ -359,9 +360,19 @@ public class RestActionMapper extends DefaultActionMapper {
namespace = ns;
}
}
if ("/".equals(ns)) {
rootAvailable = true;
}
}
// must be read before the root namespace is selected below, as it is relative to ""
name = uri.substring(namespace.length() + 1);
// WW-5688, WW-2461: still none found, use the root namespace if it is declared, so that
// an id-bearing uri lands in the same namespace as the one without an id
if (rootAvailable && namespace.isEmpty()) {
namespace = "/";
}
}
mapping.setNamespace(cleanupNamespaceName(namespace));
@@ -0,0 +1,85 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.struts2.rest;
import com.opensymphony.xwork2.XWorkTestCase;
import org.apache.struts2.config.StrutsXmlConfigurationProvider;
import org.apache.struts2.dispatcher.mapper.ActionMapping;
import org.springframework.mock.web.MockHttpServletRequest;
/**
* WW-5688: an action declared in the root namespace has to stay reachable whether or not the
* request URI carries an id, so that {@code index} and {@code show} resolve to the same action.
*
* <p>These assertions go all the way to the {@code ActionConfig} rather than stopping at the
* mapping, because the reported symptom is a 404 - the mapper handing back a namespace that
* the configuration cannot resolve.</p>
*/
public class RestActionMapperRootNamespaceTest extends XWorkTestCase {
private RestActionMapper mapper;
@Override
protected void setUp() throws Exception {
super.setUp();
loadConfigurationProviders(new StrutsXmlConfigurationProvider("ww-5688.xml"));
mapper = new RestActionMapper();
}
private ActionMapping map(String servletPath, String httpMethod) {
MockHttpServletRequest request = new MockHttpServletRequest();
request.setContextPath("/myapp");
request.setMethod(httpMethod);
request.setRequestURI("/myapp" + servletPath);
request.setServletPath(servletPath);
return mapper.getMapping(request, configurationManager);
}
private void assertResolves(String servletPath, String httpMethod, String expectedMethod) {
ActionMapping mapping = map(servletPath, httpMethod);
assertNotNull(httpMethod + " " + servletPath + " produced no mapping", mapping);
assertEquals("dog", mapping.getName());
assertEquals(expectedMethod, mapping.getMethod());
assertNotNull(httpMethod + " " + servletPath + " must resolve to the action declared in the root namespace,"
+ " but namespace '" + mapping.getNamespace() + "' does not hold it",
configurationManager.getConfiguration().getRuntimeConfiguration()
.getActionConfig(mapping.getNamespace(), mapping.getName()));
}
public void testIndexResolvesInRootNamespace() {
assertResolves("/dog", "GET", "index");
}
public void testShowResolvesInRootNamespace() {
assertResolves("/dog/1", "GET", "show");
}
public void testUpdateResolvesInRootNamespace() {
assertResolves("/dog/1", "PUT", "update");
}
public void testDestroyResolvesInRootNamespace() {
assertResolves("/dog/1", "DELETE", "destroy");
}
public void testIdIsStillExtracted() {
ActionMapping mapping = map("/dog/1", "GET");
assertEquals("1", ((String[]) mapping.getParams().get("id"))[0]);
}
}
@@ -261,6 +261,23 @@ public class RestActionMapperTest extends TestCase {
tryUri("/my/foo/23;edit", "/my", "foo/23;edit");
}
/**
* WW-5688: when a package declares the root namespace, a uri that matches no more specific
* namespace belongs to that root rather than to the default namespace, so that "/foo" and
* "/foo/23" agree on where the action lives. Without a root package the default namespace
* still wins - see {@link #testParseNameAndNamespace()}.
*/
public void testParseNameAndNamespaceWithRootPackage() {
config.addPackageConfig("root", new PackageConfig.Builder("root").namespace("/").build());
tryUri("/foo/23", "/", "foo/23");
tryUri("/foo/", "/", "foo/");
tryUri("/", "/", "");
// a longer declared namespace still outranks the root
tryUri("/my/foo/23", "/my", "foo/23");
}
public void testShouldAllowExclamation() throws Exception {
req.setRequestURI("/myapp/animals/dog/fido!edit");
req.setServletPath("/animals/dog/fido!edit");
@@ -0,0 +1,31 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!--
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
-->
<!DOCTYPE struts PUBLIC
"-//Apache Software Foundation//DTD Struts Configuration 6.0//EN"
"https://struts.apache.org/dtds/struts-6.0.dtd">
<!-- WW-5688: the only declared package sits at the root namespace -->
<struts>
<package name="ww-5688-root" namespace="/">
<action name="dog" class="org.apache.struts2.ActionSupport"/>
</package>
</struts>
+1 -1
View File
@@ -24,7 +24,7 @@
<parent>
<groupId>org.apache.struts</groupId>
<artifactId>struts2-plugins</artifactId>
<version>6.10.0</version>
<version>6.12.0-SNAPSHOT</version>
</parent>
<artifactId>struts2-sitemesh-plugin</artifactId>
+1 -1
View File
@@ -24,7 +24,7 @@
<parent>
<groupId>org.apache.struts</groupId>
<artifactId>struts2-plugins</artifactId>
<version>6.10.0</version>
<version>6.12.0-SNAPSHOT</version>
</parent>
<artifactId>struts2-spring-plugin</artifactId>
+1 -1
View File
@@ -24,7 +24,7 @@
<parent>
<groupId>org.apache.struts</groupId>
<artifactId>struts2-plugins</artifactId>
<version>6.10.0</version>
<version>6.12.0-SNAPSHOT</version>
</parent>
<artifactId>struts2-testng-plugin</artifactId>
+1 -1
View File
@@ -24,7 +24,7 @@
<parent>
<groupId>org.apache.struts</groupId>
<artifactId>struts2-plugins</artifactId>
<version>6.10.0</version>
<version>6.12.0-SNAPSHOT</version>
</parent>
<artifactId>struts2-tiles-plugin</artifactId>
@@ -26,7 +26,6 @@ import org.apache.tiles.request.ApplicationContext;
import org.apache.tiles.request.ApplicationResource;
import org.apache.tiles.request.locale.LocaleUtil;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Locale;
import java.util.Map;
@@ -53,6 +52,13 @@ public class CachingLocaleUrlDefinitionDAO extends BaseLocaleUrlDefinitionDAO im
*/
public static final String CHECK_REFRESH_INIT_PARAMETER = "org.apache.tiles.definition.dao.LocaleUrlDefinitionDAO.CHECK_REFRESH";
/**
* Default upper bound on the number of customization keys (locales) whose definitions are cached at once. Since the
* customization key is derived from the request locale, this bounds the cache so it cannot grow without limit as
* distinct locales are encountered.
*/
public static final int DEFAULT_MAX_CACHED_LOCALES = 1000;
/**
* The locale-specific set of definitions objects.
*
@@ -60,6 +66,12 @@ public class CachingLocaleUrlDefinitionDAO extends BaseLocaleUrlDefinitionDAO im
*/
protected Map<Locale, Map<String, Definition>> locale2definitionMap;
/**
* Maximum number of customization keys (locales) retained in {@link #locale2definitionMap}. When exceeded, the
* eldest entry is evicted (and reloaded on demand if requested again).
*/
protected int maxCachedLocales = DEFAULT_MAX_CACHED_LOCALES;
/**
* Flag that, when <code>true</code>, enables automatic checking of URLs
* changing.
@@ -82,7 +94,29 @@ public class CachingLocaleUrlDefinitionDAO extends BaseLocaleUrlDefinitionDAO im
*/
public CachingLocaleUrlDefinitionDAO(ApplicationContext applicationContext) {
super(applicationContext);
locale2definitionMap = new HashMap<>();
locale2definitionMap = new LinkedHashMap<Locale, Map<String, Definition>>(16, 0.75f, false) {
@Override
protected boolean removeEldestEntry(Map.Entry<Locale, Map<String, Definition>> eldest) {
if (size() <= maxCachedLocales) {
return false;
}
if (definitionResolver != null) {
definitionResolver.removePatternPaths(eldest.getKey());
}
return true;
}
};
}
/**
* Sets the maximum number of customization keys (locales) whose definitions are cached. When more distinct keys are
* requested, the eldest cached entry is evicted so the cache cannot grow without bound. Evicted entries are reloaded
* on demand if requested again, so eviction never changes rendering, only re-incurs a load.
*
* @param maxCachedLocales the maximum number of cached customization keys; values below 1 are treated as 1
*/
public void setMaxCachedLocales(int maxCachedLocales) {
this.maxCachedLocales = Math.max(1, maxCachedLocales);
}
/**
@@ -21,9 +21,9 @@ package org.apache.tiles.core.definition.pattern;
import org.apache.tiles.api.Definition;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* A pattern definition resolver that stores {@link DefinitionPatternMatcher}
@@ -39,7 +39,7 @@ public abstract class AbstractPatternDefinitionResolver<T> implements PatternDef
/**
* Stores patterns depending on the locale they refer to.
*/
private final Map<T, List<DefinitionPatternMatcher>> localePatternPaths = new HashMap<>();
private final Map<T, List<DefinitionPatternMatcher>> localePatternPaths = new ConcurrentHashMap<>();
/** {@inheritDoc} */
public Definition resolveDefinition(String name, T customizationKey) {
@@ -103,4 +103,10 @@ public abstract class AbstractPatternDefinitionResolver<T> implements PatternDef
if (localePatternPaths.get(customizationKey) != null)
localePatternPaths.get(customizationKey).clear();
}
/** {@inheritDoc} */
@Override
public void removePatternPaths(T customizationKey) {
localePatternPaths.remove(customizationKey);
}
}
@@ -60,4 +60,12 @@ public interface PatternDefinitionResolver<T> {
* @param customizationKey customization key
*/
void clearPatternPaths(T customizationKey);
/**
* Removes the stored patterns for a specific customization key entirely, including the key itself. Used when the
* owning definitions cache evicts a customization key so that the pattern store cannot grow without bound.
*
* @param customizationKey customization key
*/
void removePatternPaths(T customizationKey);
}
@@ -34,6 +34,7 @@ import org.apache.tiles.request.ApplicationResource;
import org.apache.tiles.request.locale.URLApplicationResource;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
@@ -368,4 +369,40 @@ public class CachingLocaleUrlDefinitionDAOTest extends TestCase {
assertEquals(1, attributes.size());
verify(applicationContext);
}
/**
* The definitions cache is keyed by locale, so it must not grow beyond the configured bound as distinct
* locales are requested; the eldest entry is evicted instead, and the eviction is propagated to the pattern
* resolver so its per-locale store cannot grow without bound either.
*/
public void testLocaleCacheIsBounded() {
List<ApplicationResource> sourceURLs = new ArrayList<>();
sourceURLs.add(url1);
sourceURLs.add(url2);
sourceURLs.add(url3);
definitionDao.setSources(sourceURLs);
definitionDao.setReader(new DigesterDefinitionsReader());
List<Locale> evicted = new ArrayList<>();
WildcardDefinitionPatternMatcherFactory factory = new WildcardDefinitionPatternMatcherFactory();
PatternDefinitionResolver<Locale> recordingResolver = new BasicPatternDefinitionResolver<Locale>(factory, factory) {
@Override
public void removePatternPaths(Locale customizationKey) {
evicted.add(customizationKey);
super.removePatternPaths(customizationKey);
}
};
definitionDao.setPatternDefinitionResolver(recordingResolver);
definitionDao.setMaxCachedLocales(2);
for (Locale locale : new Locale[]{Locale.US, Locale.FRENCH, Locale.CANADA_FRENCH, Locale.CHINA}) {
assertNotNull("Definitions for " + locale + " were not loaded.",
definitionDao.getDefinitions(locale));
}
assertEquals("Definitions cache must not grow beyond the configured bound",
2, definitionDao.locale2definitionMap.size());
assertEquals("Evicted locales must be removed from the pattern resolver in lockstep",
Arrays.asList(Locale.US, Locale.FRENCH), evicted);
}
}
@@ -79,6 +79,35 @@ public class AbstractPatternDefinitionResolverTest {
testResolveDefinitionImpl();
}
/**
* Test method for
* {@link AbstractPatternDefinitionResolver#removePatternPaths(Object)}: the key and its patterns are dropped
* entirely, so nothing resolves for that key afterwards.
*/
@Test
public void testRemovePatternPaths() {
firstMatcher = createMock(DefinitionPatternMatcher.class);
thirdMatcher = createMock(DefinitionPatternMatcher.class);
Definition firstDefinition = new Definition("first", null, null);
Definition firstTransformedDefinition = new Definition("firstTransformed", null, null);
expect(firstMatcher.createDefinition("firstTransformed")).andReturn(firstTransformedDefinition);
replay(firstMatcher, thirdMatcher);
Map<String, Definition> localeDefsMap = new LinkedHashMap<>();
localeDefsMap.put("first", firstDefinition);
resolver.storeDefinitionPatterns(localeDefsMap, 1);
assertEquals(firstTransformedDefinition, resolver.resolveDefinition("firstTransformed", 1));
resolver.removePatternPaths(1);
assertNull("removePatternPaths must drop the entry so nothing resolves for the key",
resolver.resolveDefinition("firstTransformed", 1));
verify(firstMatcher, thirdMatcher);
}
private void testResolveDefinitionImpl() {
firstMatcher = createMock(DefinitionPatternMatcher.class);
+1 -1
View File
@@ -24,7 +24,7 @@
<parent>
<groupId>org.apache.struts</groupId>
<artifactId>struts2-plugins</artifactId>
<version>6.10.0</version>
<version>6.12.0-SNAPSHOT</version>
</parent>
<artifactId>struts2-velocity-plugin</artifactId>
+1 -1
View File
@@ -24,7 +24,7 @@
<parent>
<groupId>org.apache.struts</groupId>
<artifactId>struts2-plugins</artifactId>
<version>6.10.0</version>
<version>6.12.0-SNAPSHOT</version>
</parent>
<artifactId>struts2-xslt-plugin</artifactId>
+17 -17
View File
@@ -29,7 +29,7 @@
<modelVersion>4.0.0</modelVersion>
<artifactId>struts2-parent</artifactId>
<version>6.10.0</version>
<version>6.12.0-SNAPSHOT</version>
<packaging>pom</packaging>
<name>Struts 2</name>
<url>https://struts.apache.org/</url>
@@ -51,7 +51,7 @@
<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>
<tag>STRUTS_6_10_0</tag>
<tag>STRUTS_6_9_0</tag>
</scm>
<issueManagement>
@@ -104,22 +104,22 @@
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.build.outputTimestamp>2026-05-25T15:13:54Z</project.build.outputTimestamp>
<project.build.outputTimestamp>2026-08-01T10:28:23Z</project.build.outputTimestamp>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
<!-- dependency versions in alphanumeric order -->
<asm.version>9.10</asm.version>
<jackson.version>2.21.2</jackson.version>
<log4j2.version>2.26.0</log4j2.version>
<asm.version>9.10.1</asm.version>
<jackson.version>2.22.2</jackson.version>
<log4j2.version>2.26.1</log4j2.version>
<ognl.version>3.3.5</ognl.version>
<slf4j.version>2.0.18</slf4j.version>
<spring.platformVersion>5.3.39</spring.platformVersion>
<tiles.version>3.0.8</tiles.version>
<tiles-request.version>1.0.7</tiles-request.version>
<maven-surefire-plugin.version>3.5.5</maven-surefire-plugin.version>
<maven-surefire-plugin.version>3.5.6</maven-surefire-plugin.version>
<hibernate-validator.version>6.2.4.Final</hibernate-validator.version>
<freemarker.version>2.3.34</freemarker.version>
<freemarker.version>2.3.35</freemarker.version>
<!-- Site generation -->
<fluido-skin.version>1.9</fluido-skin.version>
@@ -224,7 +224,7 @@
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>0.8.14</version>
<version>0.8.15</version>
<executions>
<execution>
<id>prepare-agent</id>
@@ -300,7 +300,7 @@
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-dependency-plugin</artifactId>
<version>3.10.0</version>
<version>3.11.0</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
@@ -341,7 +341,7 @@
<plugin>
<groupId>org.owasp</groupId>
<artifactId>dependency-check-maven</artifactId>
<version>12.2.0</version>
<version>13.0.0</version>
<configuration>
<suppressionFiles>
<suppressionFile>src/etc/project-suppression.xml</suppressionFile>
@@ -354,7 +354,7 @@
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-enforcer-plugin</artifactId>
<version>3.6.2</version>
<version>3.6.3</version>
<executions>
<execution>
<id>enforce</id>
@@ -407,7 +407,7 @@
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-site-plugin</artifactId>
<version>3.21.0</version>
<version>3.22.0</version>
<configuration>
<relativizeDecorationLinks>false</relativizeDecorationLinks>
</configuration>
@@ -770,7 +770,7 @@
<dependency>
<groupId>org.easymock</groupId>
<artifactId>easymock</artifactId>
<version>5.6.0</version>
<version>5.7.0</version>
<scope>test</scope>
</dependency>
@@ -846,12 +846,12 @@
<dependency>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
<version>1.3.6</version>
<version>1.4.0</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-collections4</artifactId>
<version>4.5.0</version>
<version>4.6.0</version>
</dependency>
<dependency>
<groupId>commons-fileupload</groupId>
@@ -892,7 +892,7 @@
<dependency>
<groupId>commons-validator</groupId>
<artifactId>commons-validator</artifactId>
<version>1.10.1</version>
<version>1.11.0</version>
</dependency>
<!-- Mocks for unit testing (by Spring) -->