Commit f4c473a
fix(spring): handle Unicode classpath resource paths (#24220)
## Summary
Fix Spring classpath resource matching when an application is located
under a directory whose path contains encoded Unicode or decomposed
Unicode characters.
`CustomResourceLoader` previously compared resource paths from
`URL#getPath()`, which can leave parts of the classpath root
percent-encoded. When Spring later returned class resources using a
decoded path, the resource no longer matched its parent root and startup
could fail with `Parent resource ... not found in the resources!`.
This can happen even when both paths refer to the same filesystem
location. The issue is that the compared strings are not in the same
representation:
- `URL#getPath()` can return a path where non-ASCII characters are still
percent-encoded, for example `%C3%A7` or `%CC%A7`.
- Spring resource resolution can later return the corresponding class
resource using a decoded filesystem path.
- A raw string comparison then fails, even though both paths point to
the same location.
This is especially easy to reproduce with decomposed Unicode characters.
A decomposed character is represented as a base character plus one or
more combining marks instead of a single precomposed code point. For
example, `ç` can be represented either as the single code point
`U+00E7`, or as `c` plus the combining cedilla `U+0327`.
Visually the path can look correct, but the encoded URL path and the
decoded filesystem path are different strings. The culprit was therefore
not Unicode normalization itself, but comparing URL-encoded paths with
decoded paths during parent/child classpath resource matching.
This change normalizes resource paths through `URL#toURI().getPath()`
for comparisons, while keeping the original URL path for dev-mode cache
keys. It also keeps the existing native-image `file:///resources!`
handling and falls back to the original URL path if URI conversion is
not possible.
The key piece needed to make the fix work is using the decoded URI path
for comparable resource paths:
```java
resource.getURL().toURI().getPath()
```
instead of relying on the raw URL path for matching:
```java
resource.getURL().getPath()
```
This follows the JDK recommendation for URL escaping handling. `URL`
does not itself encode or decode URL components, and the recommended way
to manage URL encoding and decoding is to use `URI` and convert between
`URL` and `URI`.
Recommended reference:
https://docs.oracle.com/javase/8/docs/api/java/net/URL.html
Closes #11871
## Implementation note
The regression test needs to exercise `CustomResourceLoader` directly. I
found existing Flow tests using both patterns: some use reflection to
reach private implementation details, and others keep implementation
types package-private so same-package tests can instantiate them
directly.
I chose to make `CustomResourceLoader` package-private instead of using
reflection, because it keeps the test simpler while still avoiding
public API exposure. If the maintainers prefer preserving the private
nested class, I can switch the test back to reflection and make
`CustomResourceLoader` private again.
The important behavioral change is limited to the comparable path used
for parent/child resource matching. The original URL path is still
preserved where the previous encoded form is required, such as dev-mode
cache lookup keys.
## Testing
- Reproduced the issue with a minimal Spring Boot/Vaadin application in
a project path containing `François` and a decomposed Unicode segment.
Before the fix, the app failed during test startup with `Parent resource
... not found in the resources!`.
- Verified the same minimal application starts/tests successfully after
installing the fixed local Flow artifacts.
- Added regression coverage in `VaadinServletContextInitializerTest` for
a classpath root containing Unicode and decomposed Unicode characters.
- Verified the regression test is meaningful: with only the production
fix temporarily reverted, the new test fails with `Parent resource ...
not found in the resources!`; with the fix restored, the same test
passes.
Local checks run:
```bash
mvn -q -P!install-git-hooks -pl vaadin-spring spotless:check
mvn -q -P!install-git-hooks -pl vaadin-spring -Dtest=VaadinServletContextInitializerTest test
mvn -q -P!install-git-hooks -pl vaadin-spring -Dtest=VaadinServletContextInitializerTest#customResourceLoader_classpathRootContainsUnicodeCombiningCharacter_resourcesAreMatched test
mvn -q -P!install-git-hooks -pl vaadin-spring -am -DskipTests -Dexec.skip=true install
```
## AI Disclosure
Code drafted with OpenClaw/Codex for contributor review. Tests were
added and run locally by the assistant. I reviewed the code and this
description before opening the PR.
Co-authored-by: Mikhail Shabarov <61410877+mshabarov@users.noreply.github.com>1 parent 3fcd491 commit f4c473a
2 files changed
Lines changed: 66 additions & 13 deletions
File tree
- vaadin-spring/src
- main/java/com/vaadin/flow/spring
- test/java/com/vaadin/flow/spring
Lines changed: 26 additions & 13 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
26 | 26 | | |
27 | 27 | | |
28 | 28 | | |
| 29 | + | |
29 | 30 | | |
30 | 31 | | |
31 | 32 | | |
| |||
976 | 977 | | |
977 | 978 | | |
978 | 979 | | |
979 | | - | |
980 | | - | |
| 980 | + | |
981 | 981 | | |
982 | 982 | | |
983 | 983 | | |
| |||
1048 | 1048 | | |
1049 | 1049 | | |
1050 | 1050 | | |
1051 | | - | |
1052 | | - | |
1053 | | - | |
1054 | | - | |
1055 | | - | |
1056 | | - | |
1057 | | - | |
1058 | | - | |
1059 | | - | |
| 1051 | + | |
| 1052 | + | |
1060 | 1053 | | |
1061 | 1054 | | |
1062 | 1055 | | |
| |||
1066 | 1059 | | |
1067 | 1060 | | |
1068 | 1061 | | |
1069 | | - | |
1070 | | - | |
| 1062 | + | |
| 1063 | + | |
1071 | 1064 | | |
1072 | 1065 | | |
1073 | 1066 | | |
| |||
1124 | 1117 | | |
1125 | 1118 | | |
1126 | 1119 | | |
| 1120 | + | |
| 1121 | + | |
| 1122 | + | |
| 1123 | + | |
| 1124 | + | |
| 1125 | + | |
| 1126 | + | |
| 1127 | + | |
| 1128 | + | |
| 1129 | + | |
| 1130 | + | |
| 1131 | + | |
| 1132 | + | |
| 1133 | + | |
| 1134 | + | |
| 1135 | + | |
| 1136 | + | |
| 1137 | + | |
| 1138 | + | |
| 1139 | + | |
1127 | 1140 | | |
1128 | 1141 | | |
1129 | 1142 | | |
| |||
Lines changed: 40 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
19 | 19 | | |
20 | 20 | | |
21 | 21 | | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
22 | 28 | | |
23 | 29 | | |
24 | 30 | | |
| |||
29 | 35 | | |
30 | 36 | | |
31 | 37 | | |
| 38 | + | |
32 | 39 | | |
33 | 40 | | |
34 | 41 | | |
35 | 42 | | |
36 | 43 | | |
37 | 44 | | |
38 | 45 | | |
| 46 | + | |
| 47 | + | |
39 | 48 | | |
40 | 49 | | |
41 | 50 | | |
| |||
53 | 62 | | |
54 | 63 | | |
55 | 64 | | |
| 65 | + | |
56 | 66 | | |
57 | 67 | | |
58 | 68 | | |
| |||
216 | 226 | | |
217 | 227 | | |
218 | 228 | | |
| 229 | + | |
| 230 | + | |
| 231 | + | |
| 232 | + | |
| 233 | + | |
| 234 | + | |
| 235 | + | |
| 236 | + | |
| 237 | + | |
| 238 | + | |
| 239 | + | |
| 240 | + | |
| 241 | + | |
| 242 | + | |
| 243 | + | |
| 244 | + | |
| 245 | + | |
| 246 | + | |
| 247 | + | |
| 248 | + | |
| 249 | + | |
| 250 | + | |
| 251 | + | |
| 252 | + | |
| 253 | + | |
| 254 | + | |
| 255 | + | |
| 256 | + | |
| 257 | + | |
| 258 | + | |
219 | 259 | | |
220 | 260 | | |
221 | 261 | | |
| |||
0 commit comments