Skip to content

Move SinkEventAttributeSet to doxia-sink-api - #1073

Merged
slachiewicz merged 3 commits into
apache:masterfrom
slachiewicz:sink-event-attribute-set-to-api
Sep 3, 2026
Merged

slachiewicz merged 3 commits into
apache:masterfrom
slachiewicz:sink-event-attribute-set-to-api

Conversation

@slachiewicz

@slachiewicz slachiewicz commented Aug 8, 2026 •

Copy link
Copy Markdown
Member

Draft implementation of #1072, to make the discussion concrete. Happy to drop it if you would rather solve this differently.

doxia-sink-api declares Sink methods that take a SinkEventAttributes, but ships no implementation of that interface and no factory for one. The only implementation lives in doxia-core under org.apache.maven.doxia.sink.impl, so every caller that wants to pass attributes to a sink has to depend on doxia-core and import from a package whose name says it is internal. That is what MSHARED-1364 / apache/maven-reporting-impl#184 is about, and it applies to most report renderers in the ecosystem, not just that one.

The class was in package org.apache.maven.doxia.sink until DOXIA-506 moved it in with the genuinely internal sink implementations.

What this does

Moves SinkEventAttributeSet and its nested Semantics to doxia-sink-api, in package org.apache.maven.doxia.sink, next to the interface it implements. Its unit test moves with it, which is why the module gains a junit-jupiter-api test dependency; it still has no compile dependencies.

The move costs the API module nothing. The class needs only javax.swing.text.AttributeSet, JDK collections and SinkEventAttributes, and SinkEventAttributes already extends javax.swing.text.MutableAttributeSet.

Binary compatibility

japicmp passes on every module, and this adds no exclusion of its own (doxia-core already carries one, for SinkUtils#filterAttributes). Three things were needed to get there, and the last one is the part worth your attention.

  1. org.apache.maven.doxia.sink.impl.SinkEventAttributeSet stays as a deprecated subclass of the new class. The static constants resolve through it, since a getstatic resolves through superclasses.

  2. It carries its own deprecated nested Semantics subclass and an override of unmodifiable(). Neither comes for free: a nested class is not inherited under its old binary name, so existing code referencing …/sink/impl/SinkEventAttributeSet$Semantics would fail with NoClassDefFoundError, and unmodifiable() declares its own type as the return type, so the inherited one has the wrong descriptor.

  3. The protected parser hooks in doxia-core that take or return this type keep taking the deprecated one, so nothing on the extension API changes:

    • AbstractXmlParser.getAttributesFromParser(XmlPullParser)
    • AbstractXmlParser.handleUnknown(String, SinkEventAttributeSet, Sink, int)
    • Xhtml5BaseParser.baseStartTag(String, SinkEventAttributeSet, Sink)
    • Xhtml5BaseParser.baseEndTag(String, SinkEventAttributeSet, Sink)
    • Xhtml5BaseParser.consecutiveSections(int, Sink, SinkEventAttributeSet)

    Retyping those would be a silent break rather than a loud one: a subclass compiled against the old signature would stop overriding the method and simply never be called again. So I left them alone. They arguably want to take the SinkEventAttributes interface rather than a concrete set at all, but that is a separate change needing a version bump, and it is not what the issue is about.

What does change: the runtime type of attribute sets

Worth being explicit, because japicmp cannot see it and it is the one thing here that is not free. Because the old class is now a subclass, the constants and every attribute set Doxia creates internally are instances of the new class only:

org.apache.maven.doxia.sink.impl.SinkEventAttributeSet.BOLD instanceof
    org.apache.maven.doxia.sink.impl.SinkEventAttributeSet   // was true, now false

Code that tests those values with instanceof, or casts them to the old class, has to move to the new one. Reading the constants through either name still yields the very same objects.

The alternative was to redeclare the constants on the deprecated class. That keeps the old runtime type, at the price of impl.SinkEventAttributeSet.BOLD != SinkEventAttributeSet.BOLD. I picked identity over the old type, on the grounds that comparing and passing these constants around is common and downcasting them is not, but it is a judgement call and I am happy to flip it. Either way it is now pinned by tests rather than left implicit, and both classes say in their javadoc which way it went.

Source compatibility is otherwise preserved, with one exception the japicmp setup does not cover (onlyBinaryIncompatible is true): a file wildcard-importing both org.apache.maven.doxia.sink.* and org.apache.maven.doxia.sink.impl.* now has an ambiguous SinkEventAttributeSet and needs a single-type import.

Why a subclass rather than a copy

MSHARED-1364 suggested copying the class and deprecating the old one. A subclass avoids two bodies of the same code drifting apart, and it is what keeps the constants identical across both names. The cost is that the new class can never become final, and the new Semantics has to keep an accessible constructor rather than becoming a proper uninstantiable constants holder, because the deprecated nested class extends it. Say the word if you would rather have the copy.

Versioning

This adds a class to a published API module, so it wants 2.2.0 rather than 2.1.1. The javadoc is written accordingly.

Follow-up

The deprecated class cannot actually be removed while those parser hooks reference it, so #1074 tracks retyping them to SinkEventAttributes in the next major and dropping the class after.

Once this is released, AbstractMavenReportRenderer in maven-reporting-impl is a one-line import change and MSHARED-1364 is done.

Verification

mvn verify is green: full test suite, rat, and japicmp across all modules.

Beyond the signature checking, SinkEventAttributeSetCompatibilityTest in doxia-core pins the runtime behaviour of the deprecated class: the constants stay reachable and identical through it, unmodifiable() keeps returning this type, clone() clones to this type, and the constants are no longer instances of it. The clone() case in particular fails against the previous implementation.

doxia-sink-api declares Sink methods taking a SinkEventAttributes but
ships no implementation of that interface and no factory for one, so
every caller that wants to pass attributes to a sink has to depend on
doxia-core and import from the .impl package. The class was in package
org.apache.maven.doxia.sink until DOXIA-506 moved it in with the sink
implementations.

Move it, along with its nested Semantics, next to the interface it
implements. It brings no new dependencies with it: it needs only
javax.swing.text.AttributeSet, JDK collections and SinkEventAttributes,
and SinkEventAttributes already extends MutableAttributeSet.

The old class stays behind as a deprecated subclass so that existing
bytecode keeps working. It carries its own nested Semantics, which is
not inherited under the old binary name, and overrides unmodifiable() to
keep the old return type. The protected parser hooks in doxia-core that
take this type keep taking the deprecated one, so nothing on the
extension API changes; japicmp passes with no exclusions.

This comment was marked as resolved.

clone() built its result with an explicit "new SinkEventAttributeSet",
so the deprecated subclass in doxia-core would have cloned to the parent
type instead of its own, and code casting the result back to the old
type would have started failing with a ClassCastException. japicmp
cannot see this, since no signature changes.

Use super.clone() and replace the shared fields afterwards, which keeps
the existing behaviour of the class and makes any subclass clone
correctly.

The added test covers the three things the deprecated class has to keep
doing that japicmp cannot express: reaching the constants, keeping the
return type of unmodifiable(), and cloning to itself.
Keeping the old class as a subclass means the constants, and every
attribute set Doxia creates internally, are now instances of the new
class only. Code that tests them with instanceof or casts them to the
old class has to move; japicmp cannot see that, since no signature
changes.

The alternative would have been to redeclare the constants on the
deprecated class, which keeps the old runtime type but breaks their
identity across the two names. The tests pin down the choice made here
so it cannot be reversed by accident, and the javadoc on both classes
says which way it went.

Also record the version the class moved in and the version the old one
was deprecated in.
@slachiewicz

Copy link
Copy Markdown
Member Author

Pushed a890a94 and expanded the description after a closer look at what the subclass approach actually costs. Three things worth surfacing rather than leaving for review to find:

The constants change runtime type. Since the static initialisers now run in the new class, impl.SinkEventAttributeSet.BOLD and friends, and every attribute set Doxia produces internally, are instances of the new class only. instanceof against the old class returns false where it used to return true. japicmp cannot see this, and neither could I until I probed the built jars. The alternative is redeclaring the constants on the deprecated class, which keeps the old type but costs their identity across the two names. I picked identity and pinned it with a test, but it is a judgement call, so please say if you would rather have it the other way.

"No exclusions" was wrong in my original description; doxia-core already carries one for SinkUtils#filterAttributes. This PR adds none of its own. Corrected.

The parser hooks make the deprecation unfulfillable in 2.x, since the class cannot be removed while they reference it. Filed #1074 for retyping them to SinkEventAttributes in the next major and dropping the class after, so this is staged rather than half-done.

Also added @since/@deprecated version markers, on the assumption this targets 2.2.0 rather than 2.1.1.

@michael-o michael-o left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Reasonable, @kwin WDYT?

@slachiewicz slachiewicz added enhancement New feature or request breaking Pull requests that break existing features labels Aug 18, 2026
@slachiewicz
slachiewicz merged commit e83c5a2 into apache:master Sep 3, 2026
11 checks passed
@github-actions github-actions Bot added this to the 2.1.1 milestone Sep 3, 2026
@slachiewicz
slachiewicz deleted the sink-event-attribute-set-to-api branch September 3, 2026 12:03
tanvir-ux added a commit to tanvir-ux/maven-doxia that referenced this pull request Sep 3, 2026
equals() already follows isEqual() and compares only own attributes. hashCode was folding in the resolve parent, so two equal sets could have different hash codes.

Rebased onto master after apache#1073 moved these files into doxia-sink-api.

Fixes apache#1075
slachiewicz pushed a commit to tanvir-ux/maven-doxia that referenced this pull request Sep 3, 2026
equals() already follows isEqual() and compares only own attributes. hashCode was folding in the resolve parent, so two equal sets could have different hash codes.

Rebased onto master after apache#1073 moved these files into doxia-sink-api.

Fixes apache#1075
slachiewicz pushed a commit that referenced this pull request Sep 3, 2026
equals() already follows isEqual() and compares only own attributes. hashCode was folding in the resolve parent, so two equal sets could have different hash codes.

Rebased onto master after #1073 moved these files into doxia-sink-api.

Fixes #1075
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

breaking Pull requests that break existing features enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants