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.entities;
  
  import java.util.Objects;
  import java.util.Optional;
  
  import org.datanucleus.ExecutionContext;
  import org.datanucleus.api.jdo.DataNucleusHelperJDO;
  import org.datanucleus.cache.CachedPC;
  import org.datanucleus.enhancement.Persistable;
  import org.datanucleus.identity.SingleFieldId;
  import org.datanucleus.metadata.AbstractClassMetaData;
  import org.datanucleus.state.ReferentialStateManagerImpl;
  import org.datanucleus.store.FieldValues;
  
  import org.jspecify.annotations.Nullable;
  
  import org.apache.causeway.commons.internal.base._Casts;
  import org.apache.causeway.commons.internal.base._Refs;
  import org.apache.causeway.core.metamodel.context.MetaModelContext;
  import org.apache.causeway.core.metamodel.spec.ObjectSpecification;
  import org.apache.causeway.persistence.jdo.datanucleus.metamodel.facets.entity.JdoEntityFacet;
  
  import lombok.extern.log4j.Log4j2;
  
  /**
   * Handles injection into JDO entities.
   * <p>
   * Installed via config property "datanucleus.objectProvider.className".
   */
  @Log4j2
  public class DnStateManagerForCauseway
  extends ReferentialStateManagerImpl {
  
      public DnStateManagerForCauseway(final ExecutionContext ec, final AbstractClassMetaData cmd) {
          super(ec, cmd);
      }
  
      @SuppressWarnings("rawtypes")
      @Override
      public void initialiseForHollow(final Object id, final FieldValues fv, final Class pcClass) {
          super.initialiseForHollow(id, fv, pcClass);
          injectServicesIfNotAlready();
      }
  
      @SuppressWarnings({ "deprecation", "rawtypes" })
      @Override
      public void initialiseForHollowAppId(final FieldValues fv, final Class pcClass) {
          super.initialiseForHollowAppId(fv, pcClass);
          injectServicesIfNotAlready();
      }
  
      @Override
      public void initialiseForHollowPreConstructed(final Object id, final Persistable pc) {
          super.initialiseForHollowPreConstructed(id, pc);
          injectServicesIfNotAlready();
      }
  
      @Override
      public void initialiseForPersistentClean(final Object id, final Persistable pc) {
          super.initialiseForPersistentClean(id, pc);
          injectServicesIfNotAlready();
      }
  
      @Override
      public void initialiseForEmbedded(final Persistable pc, final boolean copyPc) {
          super.initialiseForEmbedded(pc, copyPc);
          injectServicesIfNotAlready();
      }
  
      @Override
      public void initialiseForPersistentNew(final Persistable pc, final FieldValues preInsertChanges) {
          super.initialiseForPersistentNew(pc, preInsertChanges);
          injectServicesIfNotAlready();
      }
  
      @Override
      public void initialiseForTransactionalTransient(final Persistable pc) {
          super.initialiseForTransactionalTransient(pc);
          injectServicesIfNotAlready();
      }
  
      @Override
      public void initialiseForDetached(final Persistable pc, final Object id, final Object version) {
          super.initialiseForDetached(pc, id, version);
          injectServicesIfNotAlready();
      }
  
      @Override
      public void initialiseForPNewToBeDeleted(final Persistable pc) {
          super.initialiseForPNewToBeDeleted(pc);
          injectServicesIfNotAlready();
      }
  
      @Override
      public void initialiseForCachedPC(final CachedPC cachedPC, final Object id) {
          super.initialiseForCachedPC(cachedPC, id);
          injectServicesIfNotAlready();
      }
  
      @Override
      public void disconnect() {
          /* If a previously attached entity becomes hollow (or detached),
           * we memoize the OID in the Persistable.dnStateManager field.
           * Using a pseudo StateManager, that only acts as a holder of an OID. */
          var entityPojo = myPC; // keep local reference
          var snapshotOid = snapshotOid();
  
          super.disconnect();
  
          snapshotOid
              .ifPresent(oid->DnOidStoreAndRecoverHelper.forEntity(entityPojo).storeOid(oid));
      }
  
      // -- PREDICATES
  
      public boolean isInsertingOrInsertingCallbacks() {
          return super.isInserting()
                  || (flags & FLAG_INSERTING_CALLBACKS) != 0;
      }
  
      // -- HELPER
  
      private boolean injectionPointsResolved = false;
  
      /**
       * Returns whether injection points are resolved for myPC.
       */
      public boolean injectServicesIfNotAlready() {
          if(myPC==null) {
              this.injectionPointsResolved = false; // reset
              return true;
          }
          if(injectionPointsResolved) {
              return true;
          }
  
          MetaModelContext.instance()
          .ifPresentOrElse(
                  mmc->{
                      mmc.getServiceInjector().injectServicesInto(myPC);
                      this.injectionPointsResolved = true;
                  },
                  ()->{
                      log.warn("cannot inject services into entity of type {}, "
                              + "as there is no ServiceInjector available",
                              myPC.getClass());
                  });
  
          return injectionPointsResolved;
      }
  
      // -- SNAPSHOT OID
  
      private Optional<String> snapshotOid() {
          if(myID==null) {
              return Optional.empty();
          }
          try {
              Object id = myPC.dnGetObjectId();
              if(id==null) {
                  return Optional.empty();
              }
              var entityFacet = lookupEntityFacet().orElse(null);
              if(entityFacet==null) {
                  return Optional.empty();
              }
              if (id instanceof SingleFieldId) {
                  id = DataNucleusHelperJDO
                          .getSingleFieldIdentityForDataNucleusIdentity((SingleFieldId<?, ?>)id, myPC.getClass());
              }
              var oidIfAny = entityFacet.identifierForDnPrimaryKey(id);
              return oidIfAny;
          } catch (Exception e) {
              log.error("exception while trying to extract entity's current primary key", e);
              return Optional.empty();
          }
      }
  
      private Optional<JdoEntityFacet> lookupEntityFacet() {
          if(myPC==null) {
              return Optional.empty();
          }
          var entityFacet = MetaModelContext.instance()
              .map(MetaModelContext::getSpecificationLoader)
              .flatMap(specLoader->specLoader.specForType(myPC.getClass()))
              .flatMap(ObjectSpecification::entityFacet)
              .flatMap(facet->_Casts.castTo(JdoEntityFacet.class, facet));
          return entityFacet;
      }
  
      // -- [CAUSEWAY-3126] PRE-DIRTY NESTED LOOP PREVENTION
  
      @FunctionalInterface
      public static interface PreDirtyPropagationLock {
          void release();
          default void releaseAfter(final Runnable runnable) {
              try {
                  runnable.run();
              } finally {
                  release();
              }
          }
      }
  
      // assuming we don't need thread-safety here,
      // as each thread presumably has its own DN execution context
      private final _Refs.ObjectReference<PreDirtyPropagationLock> preDirtyPropagationLockRef =
              _Refs.objectRef(null);
  
      private final PreDirtyPropagationLock createPreDirtyPropagationLock() {
          return ()->preDirtyPropagationLockRef.set(null);
      }
  
      /**
       * Optionally provides a {@link PreDirtyPropagationLock} for pre-dirty event propagation,
       * based on whether there is NOT already a pre-dirty event in progress for the same OID.
       */
      public Optional<PreDirtyPropagationLock> acquirePreDirtyPropagationLock(final Object id) {
  
          // this algorithm is not thread-safe
          // assuming we don't need thread-safety here,
          // as each thread presumably has its own DN execution context
  
          var lockIfGranted = !Objects.equals(myID, id)
                  || preDirtyPropagationLockRef.isNotNull()
                  ? Optional.<PreDirtyPropagationLock>empty()
                  : Optional.of(preDirtyPropagationLockRef.set(createPreDirtyPropagationLock()));
  
          if(log.isDebugEnabled()) {
              log.debug("acquirePreDirtyPropagationLock({}) -> {}",
                      id, lockIfGranted.map(lock->"GRANTED").orElse("DENIED"));
          }
  
          return lockIfGranted;
      }
  
      // -- UTILITY
  
      public static Optional<DnStateManagerForCauseway> extractFrom(final @Nullable Persistable pojo) {
          return pojo!=null
                  ? _Casts.castTo(DnStateManagerForCauseway.class, pojo.dnGetStateManager())
                  : Optional.empty();
      }
  
      // --
  
      /*
  
      could be used as hooks to detect field changes ...
  
      @Override
      public void replaceField(final Persistable pc, final int fieldNumber, final Object value) {
          super.replaceField(pc, fieldNumber, value);
      }
  
      @Override
      public void replaceFieldValue(final int fieldNumber, final Object value) {
          super.replaceField(fieldNumber, value);
      }
  
      @Override
      public void replaceField(final int fieldNumber, final Object value) {
          super.replaceField(fieldNumber, value);
      }
  
      @Override
      public void replaceFieldMakeDirty(final int fieldNumber, final Object value) {
          super.replaceField(fieldNumber, value);
      }
  
      @Override
      public void replaceField(final Persistable pc, final int fieldNumber, final Object value, final boolean makeDirty) {
          super.replaceField(pc, fieldNumber, value, makeDirty);
      }
  
      @Override
      public void replaceFields(final int[] fieldNumbers, final FieldManager fm) {
          super.replaceFields(fieldNumbers, fm);
      }
  
      @Override
      public void replaceFields(final int[] fieldNumbers, final FieldManager fm, final boolean replaceWhenDirty) {
          super.replaceFields(fieldNumbers, fm, replaceWhenDirty);
      }
  
      @Override
      public void replaceNonLoadedFields(final int fieldNumbers[], final FieldManager fm) {
          super.replaceNonLoadedFields(fieldNumbers, fm);
      }
  
      @Override
      public void postCommit(final org.datanucleus.Transaction tx) {
          super.postCommit(tx);
      } */
  
  }
  