Sitelet https://github.com/apache/maven/pull/12910
Skip to content

[4.0.x] Fix #12602: Preserve Properties defaults in immutable copies - #12910

Merged
gnodet merged 3 commits into
apache:maven-4.0.xfrom
goutamadwant:fix/12602-preserve-properties-defaults
Aug 30, 2026
Merged

gnodet merged 3 commits into
apache:maven-4.0.xfrom
goutamadwant:fix/12602-preserve-properties-defaults

Conversation

@goutamadwant

Copy link
Copy Markdown
Contributor

Fixes #12602

Summary

ROProperties copied only the direct entries from the source Properties instance. Any values inherited through its defaults chain were therefore lost, causing getProperty to return null for inherited properties.

This change snapshots the effective inherited string properties into a private defaults object before copying the direct entries. This preserves standard Properties behavior while:

  • keeping inherited properties outside the direct map
  • preserving recursive defaults
  • preserving fallback through direct non-string values
  • avoiding a mutable reference to the source defaults chain
  • avoiding an additional allocation when no inherited defaults exist

The API implementation, internal XML implementation, and MDO generation template use the same behavior.

Tests

Added regression coverage for:

  • direct and inherited properties
  • recursive defaults
  • direct string overrides
  • direct non-string values with string defaults
  • direct-map size and membership semantics
  • isolation from later source-default mutations

Verification:

  • mvn clean verify
  • mvn -pl api/maven-api-xml -am -Dtest=ImmutableCollectionsTest -Dsurefire.failIfNoSpecifiedTests=false test

mvn -Prun-its clean install was also attempted. The Core IT suite did not complete locally because its extracted Maven distribution directory disappeared during execution, resulting in cascading unrelated missing-file failures. Tests executed before that point passed after the matching 4.0.x IT support artifacts were installed.

Following this checklist to help us incorporate your
contribution quickly and easily:

  • Your pull request should address just one issue, without pulling in other changes.
  • Write a pull request description that is detailed enough to understand what the pull request does, how, and why.
  • Each commit in the pull request should have a meaningful subject line and body.
    Note that commits might be squashed by a maintainer on merge.
  • Write unit tests that match behavioral changes, where the tests fail if the changes to the runtime are not applied.
    This may not always be possible but is a best-practice.
  • Run mvn verify to make sure basic checks pass.
    A more thorough check will be performed on your pull request automatically.
  • You have run the Core IT successfully.

If your pull request is about ~20 lines of code you don't need to sign an
Individual Contributor License Agreement if you are unsure
please ask on the developers list.

To make clear that you license your contribution under
the Apache License Version 2.0, January 2004
you have to acknowledge this by using the following check-box.

Preserve effective inherited string properties when creating immutable Properties snapshots. Keep defaults outside the direct map and avoid retaining mutable source references.

Add regression coverage for nested defaults, overrides, non-string shadows, and source mutation.
if (props == null) {
return null;
}
Properties defaults = null;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't follow why you don't just create defaults here. Lazy initialization is not obviously needed. What's the reasoning? Why return null when the aergument is not null?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1 on @elharo's point. The lazy init avoids an allocation when the source Properties has no defaults chain (the common case), but it trades clarity for a marginal optimization.

A simpler version would be:

private static Properties copyDefaults(Properties props) {
    if (props == null) {
        return null;
    }
    Properties defaults = new Properties();
    for (String name : props.stringPropertyNames()) {
        if (!(props.get(name) instanceof String)) {
            defaults.setProperty(name, props.getProperty(name));
        }
    }
    return defaults.isEmpty() ? null : defaults;
}

Or even just always return a (possibly empty) Properties and drop the ternary — super(emptyProperties) is harmless.

That said, the current code is correct as-is — just a readability nit.

@goutamadwant goutamadwant Aug 29, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

right @elharo @gnodet . The lazy initialization was intended to avoid allocating an empty Properties when no inherited defaults existed, but the added complexity might not be justified here. The API copy now initializes it eagerly, and I aligned the internal XML implementation and MDO template with the same behavior. Let me know thanks!

@gnodet gnodet left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice work — this is the right approach. Compared to the alternative (our #12906, now closed), this PR:

  1. Preserves the Properties contract — containsKey() returns false for defaults-only keys, while getProperty() still finds them through the chain. The flattening approach in #12906 lost this distinction.
  2. Handles non-String values — entrySet() copies direct non-String values intact, while copyDefaults() reconstructs the string fallback chain. #12906 silently dropped non-String entries.
  3. Fixes all three copies (api, impl, mdo template) — #12906 only touched the api copy.

The test is more thorough than ours too — exercises multi-level defaults, non-String shadow values, and containsKey() vs getProperty() semantics.

One minor nit (echoed on @elharo's comment): the lazy init of defaults in copyDefaults() trades clarity for a marginal optimization. Consider the simpler eager-init version. But it's correct either way — not blocking on it.

LGTM ✅

@gnodet gnodet added bug Something isn't working mvn4 labels Aug 29, 2026
@gnodet gnodet added this to the 4.0.0-rc-7 milestone Aug 29, 2026
@gnodet gnodet added the mvn40 label Aug 29, 2026
gnodet and others added 2 commits August 29, 2026 18:11
Apply the eager defaults snapshot initialization to the internal XML implementation and MDO template, matching the API implementation updated during review.
@goutamadwant

goutamadwant commented Aug 29, 2026 •

Copy link
Copy Markdown
Contributor Author

@elharo @gnodet I aligned the defaults initialization across all three maintained copies in 5617ca5. The focused regression test and full mvn clean verify pass. Please let me know if you have any further feedback. Thanks!

@gnodet
gnodet requested a review from elharo August 29, 2026 18:51
@gnodet gnodet modified the milestones: 4.0.0-rc-7, 4.1.0 Aug 30, 2026
@gnodet
gnodet changed the base branch from maven-4.0.x to master August 30, 2026 09:03
@gnodet
gnodet changed the base branch from master to maven-4.0.x August 30, 2026 09:03
@gnodet
gnodet merged commit 3ed59cd into apache:maven-4.0.x Aug 30, 2026
22 checks passed
@gnodet gnodet modified the milestones: 4.1.0, 4.0.0-rc-7, 4.0.0 Aug 30, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working mvn4 mvn40

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[maven-4.0.x] ImmutableCollections.ROProperties loses Properties defaults chain

3 participants