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.core.interaction.scope;
  
  import java.util.Map;
  import java.util.UUID;
  
  import org.springframework.beans.BeansException;
  import org.springframework.beans.factory.BeanFactory;
  import org.springframework.beans.factory.NoSuchBeanDefinitionException;
  import org.springframework.beans.factory.NoUniqueBeanDefinitionException;
  import org.springframework.beans.factory.ObjectFactory;
  import org.springframework.beans.factory.config.Scope;
  
  import org.apache.causeway.applib.services.iactnlayer.InteractionService;
  import org.apache.causeway.commons.internal.collections._Maps;
  import org.apache.causeway.commons.internal.debug._Probe;
  import org.apache.causeway.commons.internal.exceptions._Exceptions;
  
  import lombok.Data;
  import lombok.extern.log4j.Log4j2;
  
  /**
   * @since 2.0
   */
  @Log4j2
  class InteractionScope
  implements
      Scope,
      InteractionScopeLifecycleHandler {
  
      private final BeanFactory beanFactory;
  
      public InteractionScope(final BeanFactory beanFactory) {
          this.beanFactory = beanFactory;
      }
  
      @Data(staticConstructor = "of")
      private static class ScopedObject {
          final String name;
          Object instance;
          Runnable destructionCallback;
          void preDestroy() {
              log.debug("destroy causeway-interaction scoped {}", name);
              if(destructionCallback!=null) {
                  destructionCallback.run();
              }
          }
      }
  
      /**
       * An alternative design would be to store the ScopedObjects in the top-level
       * {@link org.apache.causeway.applib.services.iactn.Interaction}'s
       * {@link org.apache.causeway.applib.services.iactn.Interaction#getAttribute(Class) attributes}.
       *
       * <p>
       * Why the top-level? Because this class is only interested in that top-level interaction (see
       * {@link InteractionScopeLifecycleHandler#onTopLevelInteractionPreDestroy()}), not any of the stacked.
       * </p>
       */
      private ThreadLocal<Map<String, ScopedObject>> scopedObjects = ThreadLocal.withInitial(_Maps::newHashMap);
  
      /**
      * @return an instance of the single bean matching the required type (InteractionService)
      * @throws NoSuchBeanDefinitionException if no bean of the given type was found
      * @throws NoUniqueBeanDefinitionException if more than one bean of the given type was found
      * @throws BeansException if the bean could not be created
      */
      private InteractionService interactionService() {
          return beanFactory.getBean(InteractionService.class);
      }
  
      @Override
      public Object get(final String name, final ObjectFactory<?> objectFactory) {
  
          final InteractionService interactionService;
          try {
              interactionService = interactionService();
          } catch (Exception cause) {
              throw _Exceptions.illegalState(
                      cause,
                      "Creation of bean %s with @InteractionScope requires the "
                      + "InteractionScopeBeanFactoryPostProcessor registered and initialized.", name);
          }
  
          if(!interactionService.isInInteraction()) {
              throw _Exceptions.illegalState("Creation of bean %s with @InteractionScope requires the "
                      + "calling %s to have an open Interaction on the thread-local stack. Running into "
                      + "this issue might be caused by use of ... @Inject MyScopedBean bean ..., instead of "
                      + "... @Inject Provider<MyScopedBean> provider ...", name, _Probe.currentThreadId());
          }
  
          var existingScopedObject = scopedObjects.get().get(name);
          if(existingScopedObject!=null) {
  
              if(log.isDebugEnabled()) {
                  log.debug("INTERACTION_SCOPE [{}:{}] reuse existing {}",
                          _Probe.currentThreadId(),
                          getConversationId(),
                          Integer.toHexString(existingScopedObject.hashCode()));
              }
  
              return existingScopedObject.getInstance();
          }
  
          var newScopedObject = ScopedObject.of(name);
          scopedObjects.get().put(name, newScopedObject); // just set a stub with a name only
  
          log.debug("create new causeway-interaction scoped {}", name);
          newScopedObject.setInstance(objectFactory.getObject()); // triggers call to registerDestructionCallback
  
          if(log.isDebugEnabled()) {
              log.debug("INTERACTION_SCOPE [{}:{}] create new {}",
                  _Probe.currentThreadId(),
                  getConversationId(),
                  Integer.toHexString(newScopedObject.hashCode()));
          }
  
          return newScopedObject.getInstance();
      }
  
      @Override
      public Object remove(final String name) {
          throw new UnsupportedOperationException("use CausewayInteractionScope.removeAll instead");
      }
  
      @Override
      public void registerDestructionCallback(final String name, final Runnable callback) {
          var scopedObject = scopedObjects.get().get(name);
          if(scopedObject!=null) {
              scopedObject.setDestructionCallback(callback);
          }
          // otherwise something is off
      }
  
      @Override
      public Object resolveContextualObject(final String key) {
          // null by convention if not supported
          return null;
      }
  
      @Override
      public String getConversationId() {
          // null by convention if not supported
          return interactionService().getInteractionId()
                  .map(UUID::toString)
                  .orElse(null);
      }
  
      @Override
      public void onTopLevelInteractionOpened() {
          // nothing to do
          log.debug("INTERACTION_SCOPE opened");
      }
  
      @Override
      public void onTopLevelInteractionPreDestroy() {
          log.debug("INTERACTION_SCOPE pre-destroy");
          scopedObjects.get().values()
          .forEach(scopedObject->{
              try {
                  scopedObject.preDestroy();
              } catch (Exception e) {
                  log.error(e);
              }
          });
      }
  
      @Override
      public void onTopLevelInteractionClosed() {
          log.debug("INTERACTION_SCOPE closed");
          scopedObjects.remove();
      }
  
  }
  