record ManagedObjectViewmodel()

in core/metamodel/src/main/java/org/apache/causeway/core/metamodel/object/ManagedObjectViewmodel.java [43:208]


record ManagedObjectViewmodel(
    @NonNull ObjectSpecification objSpec,
    @NonNull TransientObjectRef<Object> pojoRef,
    @NonNull TransientObjectRef<TransactionId> txIdDuringWhichRefreshed,
    @NonNull _Lazy<Bookmark> bookmarkLazy)
implements
    ManagedObject,
    Bookmarkable.BookmarkRefreshable {

    ManagedObjectViewmodel(
            final ObjectSpecification objSpec,
            final Object pojo,
            final Optional<Bookmark> bookmarkIfKnown) {

        this(
            objSpec,
            new TransientObjectRef<>(pojo),
            new TransientObjectRef<TransactionId>(null),
            null);
        bookmarkIfKnown.ifPresent(bookmarkLazy::set);
    }

    // canonical constructor
    ManagedObjectViewmodel(
        final ObjectSpecification objSpec,
        final TransientObjectRef<Object> pojoRef,
        final TransientObjectRef<TransactionId> txIdDuringWhichRefreshed,
        final _Lazy<Bookmark> bookmarkLazy) {
        _Assert.assertTrue(objSpec.isViewModel());
        specialization().assertCompliance(objSpec, pojoRef.getObject());
        this.objSpec = objSpec;
        this.pojoRef = pojoRef;
        this.txIdDuringWhichRefreshed = txIdDuringWhichRefreshed;
        this.bookmarkLazy = _Lazy.threadSafe(()->objSpec.viewmodelFacetElseFail().serializeToBookmark(this));
    }

    @Override
    public Optional<ObjectMemento> getMemento() {
        return ObjectMemento.singular(this);
    }

    @Override
    public String getTitle() {
        return _InternalTitleUtil.titleString(
            TitleRenderRequest.forObject(this));
    }

    @Override
    public Specialization specialization() {
        return ManagedObject.Specialization.VIEWMODEL;
    }

    @Override
    public Object getPojo() {
        return pojoRef.getObject();
    }

    @Override
    public final Optional<Bookmark> getBookmark() {
        return Optional.of(bookmarkLazy.get());
    }

    @Override
    public final boolean isBookmarkMemoized() {
        return bookmarkLazy.isMemoized();
    }

    @Override
    public void invalidateBookmark() {
        bookmarkLazy.clear();
    }

    // -- REFRESH OPTIMIZATION

    /**
     * If the underlying domain object is a viewmodel, refreshes any referenced entities.
     * (Acts as a no-op otherwise.)
     * @apiNote usually should be sufficient to refresh once per interaction.
     */
    final void refreshViewmodel(final @Nullable Supplier<Bookmark> bookmarkSupplier) {
        var shouldRefresh = getTransactionService().currentTransactionId()
            .map(this::shouldRefresh)
            .orElse(true); // if there is no current transaction, refresh regardless; unexpected state, might fail later

        if(!shouldRefresh) return;

        if(isBookmarkMemoized()) {
            reloadViewmodelFromMemoizedBookmark();
        } else {
            var bookmark = bookmarkSupplier!=null
                    ? bookmarkSupplier.get()
                    : null;
            if(bookmark!=null) {
                reloadViewmodelFromBookmark(bookmark);
            }
        }
    }

    // -- OBJECT CONTRACT

    @Override
    public final boolean equals(final Object obj) {
        return obj instanceof ManagedObjectViewmodel other
            ? Objects.equals(this.objSpec().logicalTypeName(), other.objSpec().logicalTypeName())
                && Objects.equals(this.getPojo(), other.getPojo())
            : false;
    }

    @Override
    public final int hashCode() {
        return Objects.hash(objSpec().logicalTypeName(), getPojo());
    }

    @Override
    public final String toString() {
        return "ManagedObjectViewmodel[logicalTypeName=%s]".formatted(objSpec().logicalTypeName());
    }

    // -- HELPER

    private void replaceBookmark(final UnaryOperator<Bookmark> replacer) {
        final Bookmark old = bookmarkLazy.isMemoized()
                ? bookmarkLazy.get()
                : null;
        bookmarkLazy.clear();
        bookmarkLazy.set(replacer.apply(old));
    }

    private boolean shouldRefresh(final @NonNull TransactionId transactionId) {
        // if already refreshed within current transaction, skip
        if(Objects.equals(this.txIdDuringWhichRefreshed.getObject(), transactionId)) return false;
        this.txIdDuringWhichRefreshed.update(__->transactionId);
        return true;
    }

    /**
     * Reload current viewmodel object from memoized bookmark, otherwise does nothing.
     */
    private void reloadViewmodelFromMemoizedBookmark() {
        var bookmark = getBookmark().get();
        var viewModelClass = getCorrespondingClass();

        var recreatedViewmodel =
                getFactoryService().viewModel(viewModelClass, bookmark);

        _XrayEvent.event("Viewmodel '%s' recreated from memoized bookmark.", viewModelClass.getName());

        replacePojo(old->recreatedViewmodel);
    }

    private void reloadViewmodelFromBookmark(final @NonNull Bookmark bookmark) {
        var viewModelClass = getCorrespondingClass();
        var recreatedViewmodel =
                getFactoryService().viewModel(viewModelClass, bookmark);

        _XrayEvent.event("Viewmodel '%s' recreated from provided bookmark.", viewModelClass.getName());

        replacePojo(old->recreatedViewmodel);
        replaceBookmark(old->bookmark);
    }

    private void replacePojo(final UnaryOperator<Object> replacer) {
        pojoRef.update(pojo->specialization().assertCompliance(objSpec, replacer.apply(pojo)));
    }

}