[DISCUSSION - DO NOT MERGE] Can the Permissionable hierarchy be sealed? (#34154) - #36992
[DISCUSSION - DO NOT MERGE] Can the Permissionable hierarchy be sealed? (#34154)#36992fabrizzio-dotCMS wants to merge 1 commit into
Conversation
…34154) NOT FOR MERGE. Groundwork for the Java 25 talk. Three compilations that answer a question the codebase keeps raising: resolvePermissionType dispatches over Permissionable and ends in a default, so the day someone adds an asset type the code keeps compiling and the new type quietly takes the default path. Sealing is what would remove that default, and with it the silence. The sources live under docs/ and outside every Maven source root on purpose: two of the three experiments are supposed to fail to compile, and the failures are the result. run.sh reproduces all three with nothing but a JDK 22 — no Maven, no dotCMS classpath, no network. Sealing it where it lives fails with one error per permitted subtype: a sealed type in the unnamed module requires every permitted subtype in the same package, and dotCMS has no module-info. Against the real type that is fourteen errors, one per implementor, spread over nine packages. Moving them into one package is not an import refactor either — the canonical names are persisted data, hardcoded even in this class's SQL. The same sources inside a named module compile, and the resolver drops its default. Adding a fifteenth permitted type then breaks that resolver without anyone touching it. So the measurement: sealing this hierarchy is blocked by neither design nor size. It costs a module-info.java, and in exchange the compiler names every place that needs updating when an asset type is added. The README closes on the limit, because it matters more than the win: half this resolver's branches dispatch on a content type stored in the database rather than on a Java type. Sealed verifies the variants that live in the type system; dotCMS keeps its variants in a table. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
❌ Issue Linking RequiredThis PR could not be linked to an issue. All PRs must be linked to an issue for tracking purposes. How to fix this:Option 1: Add keyword to PR body (Recommended - auto-removes this comment)
Option 2: Link via GitHub UI (Note: won't clear the failed check)
Why is this required?Issue linking ensures proper tracking, documentation, and helps maintain project history. It connects your code changes to the problem they solve. |
|
Claude finished @fabrizzio-dotCMS's task in 1m 45s —— View job ReviewThis is a documentation/experiment PR (
I could not run New Issues
Nits (non-blocking)
Net: no bugs in the Java, the argument holds, and as a discussion artifact it does its job. The only thing I'd tighten before this becomes a reference people copy from is the undocumented |
Can the
Permissionablehierarchy be sealed?Groundwork for the Java 25 talk (#34154). Nothing here is production code and nothing here is
built — these sources live outside every Maven source root on purpose, because two of the three
experiments are supposed to fail to compile. The failures are the result.
./run.sh # needs only a JDK 22+ — no Maven, no dotCMS classpath, no networkEverything quoted below is that script's output.
Why the question comes up
PermissionBitFactoryImpl.resolvePermissionTypedispatches overPermissionablewith a patternswitchthat ends in adefault. Adefaultis a catch-all: the day someone adds an asset type,the code keeps compiling and the new type quietly takes the default path.
Sealing the hierarchy is what would let that
defaultgo away, and with it the silence — a sealedtype tells the compiler the complete list of subtypes, so it can check that a switch covers them all.
The hierarchy is not large: 14 direct implementors of
Permissionable, 7 direct subclasses ofInode. Perfectly listable. So the question is a fair one.The setup
src/mirrors the real package layout with a handful of the real types, because the package layoutis the whole point:
Permissionablecom.dotmarketing.businessContentletcom.dotmarketing.portlets.contentlet.modelsealed, seals further downHostcom.dotmarketing.beansfinal— a leafFoldercom.dotmarketing.portlets.folders.modelfinalIdentifiercom.dotmarketing.beansfinalInodecom.dotmarketing.beansnon-sealed— gives up and reopensPermissionResolvercom.dotmarketing.businessdefaultInodebeingnon-sealeddoes not break exhaustiveness downstream: every subclass ofInodeisstill an
Inode, so onecase Inodecovers all of them. Sealing reasons about permitted subtypes,not about leaves.
Experiment 1 — seal it where it lives
Compile those sources without
module-info.java, which is the situation dotCMS is in today:One error per permitted subtype. The rule: a sealed type in the unnamed module requires every
permitted subtype to live in the same package. dotCMS has no
module-info.java, so everything isin the unnamed module.
Running the same attempt against the real
Permissionable, with all 14 implementors in thepermitsclause, gives 14 errors — one per type, no exceptions: none of the fourteen lives incom.dotmarketing.business; they are spread across nine packages.Why "just move them into one package" is not the answer
Their canonical names are persisted data.
permission_reference.permission_typestores stringslike
com.dotmarketing.portlets.folders.model.Folder, and those literals appear hardcoded inPermissionBitFactoryImpl's own SQL (lines 303 and 462). MovingFolderto another package is adata migration on the permissions table, not an import refactor.
Experiment 2 — the same sources, inside a named module
Add six lines and change nothing else:
It compiles. Sealed across packages,
Contentletsealing down toHost,Inodereopening itsbranch with
non-sealed— and the resolver carries nodefault:(
HostprecedesContentletbecause it extends it — the other order is rejected with "this caselabel is dominated by a preceding case label".)
Experiment 3 — add an asset type, touch nothing else
A fifteenth permitted type joins the
permitsclause. The resolver is left exactly as it was:That is the entire payoff, and it only exists because there is no
default. Put adefaultbackand this compiles in silence — sealing does not give you the check; removing
defaultgives youthe check, and sealing is what makes removing it possible. Any total pattern (
case Object o,case Permissionable p) silences it just the same.What this measures
Which reframes the modularisation discussion as a trade with a price tag instead of an abstract
preference. The price is real:
module-infoon a WAR carrying OSGi, Hibernate, reflection and splitpackages is a project of its own, and JPMS and OSGi are two module systems competing for the same
job.
The honest limit, worth knowing before anyone gets excited
Even fully sealed, this particular resolver would keep most of its shape, because half its
branches do not dispatch on Java types at all. From the test suite on #36982:
A contentlet "of type Host" is usually a plain
Contentletwhose content type — a row in thedatabase — is named
Host. Hence the two branches for one concept:Sealed types verify the variants that live in the type system. dotCMS's variants live in the
database. The
whenguard exists precisely because that variability escaped the type system — and itis why that method's
defaultis honesty rather than laziness.Related: #34154 · #36982 (the pattern
switchthat raised the question)🤖 Generated with Claude Code