major
#29840
Persistent object wrapper hierarchy: remove dead and duplicated code, move stateless behavior into interface defaults, replace class identity checks
Context
A persistent object is a KnowledgeObject plus a Java object implementing TLObject on top of it. The class chain between the two is PersistentObject → AbstractWrapper → AbstractBoundWrapper and then, in tl-element, AttributedWrapper and WrapperTLElement → StructuredElementWrapper → AttributedStructuredElementWrapper. The typed interfaces generated for a model (ABase, A`) are default methods over the `TLObject API; the implementation classes only attach them to a persistent object.
An inventory of the chain (bytecode reference scan over the whole reactor plus source grep, 2026-10-13) shows:
- The only state in the chain is the KnowledgeObject reference and one lazily cached DataAccessProxy (AbstractWrapper.dap). Everything else is behavior over tHandle().
- About 25 members of AbstractWrapper have no caller at all (single-partner association helpers and their private chain, getKOValue/`setKOValue`, setSecurity, hasIncoming, revalidate, isValid, idSet/`idArray`, both getWrappersFromIterator, static getReference(TLObject, Class, String), instance resolveOrderedValue, instance associateWith, setDouble, getWrappersFromAssociations(KAIterator, Filter, Filter)). Also dead: WrapperTLElement.USE_CACHE, StructuredElementWrapper.tGetFactory/traverseUp/preloadParents, the StructuredElement.addChild/removeChild defaults, WrapperFactory.getWrapper(TLID, String[]), getWrapper(Branch, Revision, TLID, String[]), hasWrapper(..), getWrapperMapping, and the classes ItemWrapperAdapter and TLObjectProxy.
- Five overrides in AbstractWrapper are identical to the interface defaults they override (getValue, setValue, getKnowledgeBase, tValid, touch = tTouch).
- The ~20 one-line delegations to PersistentObjectImpl/`PersistentScope` (tType, tContainer, tValue, tUpdate, tAdd, tRemove, getValue, setValue, generateFullText, the MetaElementHolder and TLScope methods) exist 19 times: in AttributedWrapper, AttributedStructuredElementWrapper and 17 hand-written copies (AttributedPerson, AttributedGroup, AttributedDocument, …, PersistentClassifier, PersistentEnumeration, DynamicType, DynamicModelPart, IMAPMail, IMAPMailFolder, …).
- The typed accessors (getString(String), …), the resolveLinks/`resolveWrappers` forwarders, compareTo, getAllValues, getDSN and all of AbstractBoundWrapper are pure functions of this.
- Engine code identifies persistent objects by class: instanceof PersistentObject in WrappedKnowledgeItem and ReferenceDeletionHelper (lifecycle hooks), instanceof AbstractBoundWrapper in BoundHelper, BoundSecurityFilter, Group.bind, DocumentJournalSupport, DocumentTileFormBuilder, instanceof WrapperTLElement inside the default method StructuredElement.move, casts to AbstractWrapper in DumpWriter, WrapperMetaAttributeUtil, AbstractGanttChartCreator, ExpressionSelectorComponent, CommentDialogComponent.
Goal
The chain carries no behavior that a TLObject implementation could not get from interface default methods, and the engine does not depend on the identity of these classes. This is the precondition for providing the typed interfaces for all TLObject implementations (transient objects, form overlays) and, later, for replacing the generated implementation classes.
Solution
Four steps, each building and tested on its own:
- Dead code. The members and classes listed above are deleted; the five overrides duplicating interface defaults are removed (callers of touch() use tTouch()).
- One copy of the persistent delegations. Two default-method interfaces in tl-element carry the delegations: AttributedObject extends Wrapper (type, container, attribute access, full text, attribute names, over PersistentObjectImpl) and PersistentTLScope extends MetaElementHolder (the MetaElementHolder and TLScope methods over PersistentScope). They are separate because engine code branches on instanceof TLScope, which the attributed classes are not. AttributedWrapper and AttributedStructuredElementWrapper implement both, PersistentScope the second, and the copies in AttributedAddress, AttributedCurrency, AttributedDocument, AttributedDocumentVersion, AttributedGroup, AttributedPerson, AttributedRole, AttributedUnit, AttributedWebFolder, PersistentView, IMAPMail and IMAPMailFolder are removed. Where a superclass declares one of these methods itself (a class method always wins over an interface default), the class keeps a one-line override forwarding to the interface default (AttributedDocument.getValue, AttributedAddress.generateFullText); for the same reason AbstractWrapper.tValue, tUpdate and getAllAttributeNames become Wrapper defaults. AttributedPerson and PersistentView support tAdd/`tRemove` like every attributed object (they threw UnsupportedOperationException before). The model-part classes DynamicModelPart, DynamicType, PersistentClassifier, PersistentEnumeration and PersistentStructuredTypePart keep their delegations: their tType() comes from PersistentModelPart, so they never were copies of AttributedWrapper.
- Stateless behavior as defaults. The typed accessors, getReference/`setReference`, the resolveLinks/`resolveWrappers`/`updateOrderedLinkSet` forwarders, getOutgoingWrappers/`getIncomingWrappers`, getAllValues, compareTo and getDSN are Wrapper defaults (the static helpers stay in AbstractWrapper). The AbstractBoundWrapper methods (getID, the role lookups, getSecurityChildren, getSecurityParent, getSecurityParents) are defaults of the interface BoundWrapper extends Wrapper, BoundObject; BoundObject itself is unchanged. Group and BoundedRole declare getID() explicitly because IGroup and BoundRole declare their own abstract getID(); PersonalConfigurationWrapper declares the three boolean accessors it also inherits as defaults from PersonalConfiguration.
- No class identity. The lifecycle hooks handleLoad, handleDelete, notifyUpcomingDeletion and refetchWrapper are the interface WrapperLifecycle (default no-ops) that WrappedKnowledgeItem, ReferenceDeletionHelper and DBKnowledgeBase call on the application object of an item; Wrapper extends it, PersistentObject implements it. StructuredElement.move rejects only a null child. The remaining engine sites use the interface they need: BoundObject/`BoundWrapper` instead of AbstractBoundWrapper (BoundHelper, BoundSecurityFilter, Group.bind, DocumentJournalSupport, DocumentTileFormBuilder), Wrapper instead of AbstractWrapper (DumpWriter, WrapperMetaAttributeUtil, AbstractGanttChartCreator, CommentDialogComponent, MapBasedPersistancySupport, SaveExpressionCommand, WebFolder.getOwners, the journal delegates, PersistentObjectImpl.setMetaElement), AttributedObject instead of AttributedWrapper (ContactApplyHandler, the create handlers, AbstractWrapperResolver), StructuredElement/`MetaElementHolder` instead of StructuredElementWrapper/`AttributedStructuredElementWrapper` (StructuredElementWrapperFactory, StructuredElementAttributeFilter, AttributedSearchComponent, StructureProperty, StructuredElementDOImportHandler, ScriptingElementUtil).
What remains in the classes: the KnowledgeObject reference, the DataAccessProxy cache with its reset, the (KnowledgeObject) constructor, toString/`equals`/`hashCode`, the static helpers, and the fields of the hand-written subclasses that keep state of their own. ImplementationTemplateGenerator still generates classes extending AttributedWrapper/`AttributedStructuredElementWrapper`.
Out of scope: the typed interface for transient objects and form overlays, and removing the generated implementation classes (separate ticket, enabled by this one).
Migration
Application code that calls one of the deleted members of AbstractWrapper, StructuredElementWrapper or WrapperFactory (see the list under Context) no longer compiles; these members had no caller in the engine. touch() is replaced by TLObject.tTouch(). The protected lifecycle hooks handleLoad(), handleDelete() and notifyUpcomingDeletion() are public interface methods of WrapperLifecycle; an override in application code must be declared public. The protected forwarders resolveWrappers, resolveWrappersTyped, resolveLinks and updateOrderedLinkSet are public Wrapper defaults; the typed accessors (getString(String), …) and the AbstractBoundWrapper methods are no longer final. A hand-written class that overrides getValue(String)/`setValue(String, Object)` or tValue/`tUpdate` keeps working. Application code checking instanceof AbstractBoundWrapper or instanceof AbstractWrapper should use BoundObject/`BoundWrapper` respectively Wrapper.