record ManagedObjectEntity()

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


record ManagedObjectEntity(
    @NonNull ObjectSpecification objSpec,
    /**
     * One of {ManagedObjectEntityTransient, ManagedObjectEntityBookmarked, ManagedObjectEntityRemoved}.
     * <p>
     * May dynamically mutate from 'left' to 'right' based on pojo's persistent state.
     * However, the pojo reference must be kept identical, unless the entity becomes 'removed',
     * in which case the pojo reference is invalidated and should no longer be accessible to callers.
     */
    @NonNull TransientObjectRef<EntityPhase> phaseRef)
implements ManagedObject {

    enum PhaseState {
        /** Has no bookmark yet; can be transitioned to BOOKMARKED once
         *  for accompanied pojo, an OID becomes available. */
        TRANSIENT,
        /** We have an OID,
         * regardless of the accompanied pojo's persistent state (unless becomes removed). */
        BOOKMARKED,
        /** Final state, that can be entered once after we had an OID. */
        REMOVED;
        public boolean isTransient() { return this == TRANSIENT; }
        public boolean isBookmarked() { return this == BOOKMARKED; }
        public boolean isRemoved() { return this == REMOVED; }
        /**
         * Gives advice on whether a phase transition is required, based on given {@link EntityState}.
         */
        private void reassessPhase(final EntityState newEntityState, final Consumer<PhaseState> onNewPhaseRequired) {
            transitionAdvice(this, newEntityState)
                .ifPresent(newPhase->onNewPhaseRequired.accept(newPhase));
        }
        private static Optional<PhaseState> transitionAdvice(final PhaseState previous, final EntityState entityState) {
            return switch (previous) {
                case TRANSIENT->{
                    var stayTransient = !entityState.isRemoved()
                        && !entityState.isAttached();
                    if(stayTransient) yield Optional.empty();
                    if(entityState.hasOid()) yield Optional.of(PhaseState.BOOKMARKED);
                    if(entityState.isTransientOrRemoved()) yield Optional.of(PhaseState.REMOVED);
                    yield Optional.empty();
                }
                case BOOKMARKED->entityState.isTransientOrRemoved()
                    ? Optional.of(PhaseState.REMOVED)
                    : Optional.empty();
                case REMOVED->Optional.empty();
            };
        }
    }

    ManagedObjectEntity(
            final @NonNull EntityPhaseTransient transientPhase) {
        this(transientPhase.objSpec(), new TransientObjectRef<>(transientPhase));
    }

    ManagedObjectEntity(
            final @NonNull EntityPhaseBookmarked bookmarkedPhase) {
        this(bookmarkedPhase.objSpec(), new TransientObjectRef<>(bookmarkedPhase));
    }

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

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

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

    @Override
    public Optional<Bookmark> getBookmark() {
        return (phase() instanceof EntityPhaseBookmarked bookmarked)
                ? Optional.of(bookmarked.bookmark())
                : Optional.empty();
    }

    @Override
    public boolean isBookmarkMemoized() {
        return phaseState().isBookmarked();
    }

    @Override
    public @NonNull EntityState getEntityState() {
        var entityState = phase().reassessEntityState();
        phaseState().reassessPhase(entityState, this::transition);
        return entityState;
    }

    @Override @SneakyThrows
    public Object getPojo() {
        return switch (phaseState()) {
            case TRANSIENT, BOOKMARKED -> {
                try {
                    var entityState = phase().reassessEntityState();
                    phaseState().reassessPhase(entityState, this::transition);
                    yield phase().getPojo(entityState);
                } catch (ObjectNotFoundException e) {
                    // if object not found, transition to 'removed' state
                    transition(PhaseState.REMOVED);
                    yield null;
                }
            }
            case REMOVED -> null; // don't reassess
        };
    }

    public Object peekAtPojo() {
        return phase().peekAtPojo();
    }

    // -- OBJECT CONTRACT

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

    @Override
    public final int hashCode() {
        return Objects.hash(
            objSpec().logicalTypeName(),
            phaseState(),
            getBookmark().map(Bookmark::identifier).orElse(null));
    }

    @Override
    public final String toString() {
        return "ManagedObjectEntity[logicalTypeName=%s,state=%s%s]"
            .formatted(
                objSpec().logicalTypeName(),
                phaseState().name(),
                getBookmark()
                    .map(Bookmark::identifier)
                    .map(",id=%s"::formatted)
                    .orElse(""));
    }

    // -- HELPER

    private EntityPhase phase() { return phaseRef.getObject(); }
    private PhaseState phaseState() { return phase().phaseState(); }

    private synchronized void transition(final PhaseState newPhaseState) {
        log.debug("about to transition phase from {} to {}", phaseState().name(), newPhaseState.name());
        switch (newPhaseState) {
            case BOOKMARKED -> phaseRef.update(__->new EntityPhaseBookmarked(objSpec(), peekAtPojo()));
            case REMOVED -> phaseRef.update(__->new EntityPhaseRemoved());
            case TRANSIENT -> {
                throw new UnsupportedOperationException("cannot transition to TRANSIENT (TRANSIENT is an initial state only)");
            }
        }
        _Assert.assertEquals(newPhaseState, phaseState(), ()->"transition failed");
    }

}