content: /*
   *  Licensed to the Apache Software Foundation (ASF) under one
   *  or more contributor license agreements.  See the NOTICE file
   *  distributed with this work for additional information
   *  regarding copyright ownership.  The ASF licenses this file
   *  to you under the Apache License, Version 2.0 (the
   *  "License"); you may not use this file except in compliance
   *  with the License.  You may obtain a copy of the License at
   *
   *        http://www.apache.org/licenses/LICENSE-2.0
   *
   *  Unless required by applicable law or agreed to in writing,
   *  software distributed under the License is distributed on an
   *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
   *  KIND, either express or implied.  See the License for the
   *  specific language governing permissions and limitations
   *  under the License.
   */
  package org.apache.causeway.persistence.jdo.datanucleus.changetracking;
  
  import javax.jdo.listener.AttachLifecycleListener;
  import javax.jdo.listener.ClearLifecycleListener;
  import javax.jdo.listener.CreateLifecycleListener;
  import javax.jdo.listener.DeleteLifecycleListener;
  import javax.jdo.listener.DetachLifecycleListener;
  import javax.jdo.listener.DirtyLifecycleListener;
  import javax.jdo.listener.InstanceLifecycleEvent;
  import javax.jdo.listener.LoadLifecycleListener;
  import javax.jdo.listener.StoreLifecycleListener;
  
  import org.datanucleus.enhancement.Persistable;
  
  import org.apache.causeway.applib.annotation.Domain;
  import org.apache.causeway.commons.internal.assertions._Assert;
  import org.apache.causeway.core.metamodel.context.MetaModelContext;
  import org.apache.causeway.core.metamodel.object.ManagedObject;
  import org.apache.causeway.core.metamodel.services.objectlifecycle.ObjectLifecyclePublisher;
  import org.apache.causeway.core.metamodel.services.objectlifecycle.PropertyChangeRecord;
  import org.apache.causeway.persistence.jdo.datanucleus.entities.DnStateManagerForCauseway;
  
  import org.jspecify.annotations.NonNull;
  import lombok.RequiredArgsConstructor;
  
  import lombok.extern.log4j.Log4j2;
  
  /**
   * <ul>
   * <li>enlistCreated <-> postStore (when NEW)</li>
   * <li>enlistDeleting <-> preDelete</li>
   * <li>enlistUpdating <-> preDirty</li>
   * <li>recognizeLoaded <-> postLoad</li>
   * <li>recognizePersisting <-> preStore (when NEW)</li>
   * <li>recognizeUpdating <-> postStore (when NOT NEW)</li>
   * </ul>
   *
   * @since 2.0 {@index}
   */
  @Domain.Exclude // managed by causeway
  @RequiredArgsConstructor
  @Log4j2
  public class JdoLifecycleListener
  implements AttachLifecycleListener, ClearLifecycleListener, CreateLifecycleListener, DeleteLifecycleListener,
  DetachLifecycleListener, DirtyLifecycleListener, LoadLifecycleListener, StoreLifecycleListener {
  
      private final @NonNull MetaModelContext metaModelContext;
      private final @NonNull ObjectLifecyclePublisher objectLifecyclePublisher;
  
      // -- CALLBACKS
  
      @Override
      public void postCreate(final InstanceLifecycleEvent event) {
          log.debug("postCreate {}", ()->_Utils.debug(event));
          _Utils.resolveInjectionPoints(metaModelContext, event);
      }
  
      @Override
      public void preAttach(final InstanceLifecycleEvent event) {
          log.debug("preAttach {}", ()->_Utils.debug(event));
      }
  
      @Override
      public void postAttach(final InstanceLifecycleEvent event) {
          log.debug("postAttach {}", ()->_Utils.debug(event));
          _Utils.resolveInjectionPoints(metaModelContext, event);
      }
  
      @Override
      public void postLoad(final InstanceLifecycleEvent event) {
          log.debug("postLoad {}", ()->_Utils.debug(event));
  
          final Persistable pojo = _Utils.persistableFor(event);
          var entity = adaptEntity(pojo);
  
          // it seems JDO only calls this once per transaction and entity
          // so no need to keep track of OIDs that had already been loaded
          objectLifecyclePublisher.onPostLoad(entity);
      }
  
      @Override
      public void preStore(final InstanceLifecycleEvent event) {
          log.debug("preStore {}", ()->_Utils.debug(event));
  
          final Persistable pojo = _Utils.persistableFor(event);
          var isInserting = isInserting(pojo); // check before adapting
          var entity = adaptEntity(pojo);
  
          /* Called either when an entity is initially persisted,
           * or when an entity is updated; fires the appropriate
           * lifecycle callback. So filter for those events when initially persisting. */
          if(isInserting) {
              // well but then we need an OID, so we distinguish between either we have one or not
              objectLifecyclePublisher.onPrePersist(
                      entity.asEitherWithOrWithoutMemoizedBookmark());
          } else {
              // not here, using preDirty for that instead
              //objectLifecyclePublisher.onPreUpdate(entity, /*propertyChangeRecordSupplier*/ null);
          }
      }
  
      @Override
      public void postStore(final InstanceLifecycleEvent event) {
          log.debug("postStore {}", ()->_Utils.debug(event));
  
          final Persistable pojo = _Utils.persistableFor(event);
          var isInserting = isInserting(pojo); // check before adapting
  
          var entity = adaptEntity(pojo);
  
          /* Called either when an entity is initially persisted, or when an entity is updated;
           * fires the appropriate lifecycle callback.*/
  
          if(isInserting) {
              objectLifecyclePublisher.onPostPersist(entity);
          } else {
              // the callback and transaction.enlist are done in the preStore callback
              // (can't be done here, as the enlist requires to capture the 'before' values)
              objectLifecyclePublisher.onPostUpdate(entity);
          }
      }
  
      @Override
      public void preDirty(final InstanceLifecycleEvent event) {
          log.debug("preDirty {}", ()->_Utils.debug(event));
  
          final Persistable pojo = _Utils.persistableFor(event);
          final Runnable doPreDirty = ()->doPreDirty(pojo);
  
          // [CAUSEWAY-3126] pre-dirty nested loop prevention,
          // assuming we can cast the DN StateManager to the custom one as provided by the framework
          DnStateManagerForCauseway.extractFrom(pojo).ifPresentOrElse(
                  stateManager->
                      stateManager.acquirePreDirtyPropagationLock(pojo.dnGetObjectId())
                      .ifPresent(lock->lock.releaseAfter(doPreDirty)),
                  doPreDirty);
      }
  
      /**
       * Besides triggering UPDATING lifecycle events, also enlists entity property changes, if enabled.
       *
       * @implNote {@code propertyChangeRecordSupplier} is an optional parameter to provide the pre-computed
       * {@link PropertyChangeRecord}s from the ORM. JPA does this, JDO does not.
       *
       * @see ObjectLifecyclePublisher#onPreUpdate(ManagedObject, java.util.function.Function)
       * @see org.apache.causeway.persistence.jpa.applib.integration.CausewayEntityListener#onPreUpdate(Object)
       * @see org.apache.causeway.persistence.jpa.applib.integration.CausewayEntityListener#gatherPropertyChangeRecords(ManagedObject)
       */
      private final void doPreDirty(final Persistable pojo) {
          var entity = adaptEntity(pojo);
          objectLifecyclePublisher.onPreUpdate(entity, /*propertyChangeRecordSupplier*/ null);
      }
  
      @Override
      public void postDirty(final InstanceLifecycleEvent event) {
          // not used (historically also not in v1)
      }
  
      @Override
      public void preDelete(final InstanceLifecycleEvent event) {
          log.debug("preDelete {}", ()->_Utils.debug(event));
  
          final Persistable pojo = _Utils.persistableFor(event);
          _Assert.assertNotNull(pojo.dnGetObjectId());
          var entity = adaptEntity(pojo);
  
          objectLifecyclePublisher.onPreRemove(entity);
      }
  
      @Override
      public void postDelete(final InstanceLifecycleEvent event) {
          log.debug("postDelete {}", ()->_Utils.debug(event));
      }
  
      /**
       * Does nothing, not important event for Causeway to track.
       */
      @Override
      public void preClear(final InstanceLifecycleEvent event) {
          log.debug("preClear {}", ()->_Utils.debug(event));
      }
  
      /**
       * Does nothing, not important event for Causeway to track.
       */
      @Override
      public void postClear(final InstanceLifecycleEvent event) {
          log.debug("postClear {}", ()->_Utils.debug(event));
      }
  
      @Override
      public void preDetach(final InstanceLifecycleEvent event) {
          log.debug("preDetach {}", ()->_Utils.debug(event));
      }
  
      @Override
      public void postDetach(final InstanceLifecycleEvent event) {
          log.debug("postDetach {}", ()->_Utils.debug(event));
          _Utils.resolveInjectionPoints(metaModelContext, event);
      }
  
      // -- HELPER
  
      private ManagedObject adaptEntity(
              final @NonNull Persistable pojo) {
          return _Utils.adaptEntity(metaModelContext, pojo);
      }
  
      private boolean isInserting(
              final @NonNull Persistable pojo) {
          return DnStateManagerForCauseway.extractFrom(pojo)
                  .map(DnStateManagerForCauseway::isInsertingOrInsertingCallbacks)
                  .orElse(false);
      }
  
  }