/**
   * If we have a work instance we have to check whether the instance to be indexed is contained in
   * any other indexed entities for a tenant.
   *
   * @param instance the instance to be indexed
   * @param workPlan the current work plan
   * @param currentRecursionContext the current {@link
   *     org.hibernate.search.engine.spi.ContainedInRecursionContext} object used to check the graph
   *     traversal
   * @param tenantIdentifier the identifier of the tenant or null, if there isn't one
   * @see #appendContainedInWorkForInstance(Object, WorkPlan, ContainedInRecursionContext)
   */
  public void appendContainedInWorkForInstance(
      Object instance,
      WorkPlan workPlan,
      ContainedInRecursionContext currentRecursionContext,
      String tenantIdentifier) {
    for (ContainedInMetadata containedInMetadata : typeMetadata.getContainedInMetadata()) {
      XMember member = containedInMetadata.getContainedInMember();
      Object unproxiedInstance = instanceInitializer.unproxy(instance);

      ContainedInRecursionContext recursionContext =
          updateContainedInRecursionContext(
              unproxiedInstance, containedInMetadata, currentRecursionContext);

      if (recursionContext.isTerminal()) {
        continue;
      }

      Object value = ReflectionHelper.getMemberValue(unproxiedInstance, member);

      if (value == null) {
        continue;
      }

      if (member.isArray()) {
        Object[] array = (Object[]) value;
        for (Object arrayValue : array) {
          processSingleContainedInInstance(
              workPlan, arrayValue, recursionContext, tenantIdentifier);
        }
      } else if (member.isCollection()) {
        Collection<?> collection = null;
        try {
          collection = getActualCollection(member, value);
          collection.size(); // load it
        } catch (Exception e) {
          if (e.getClass().getName().contains("org.hibernate.LazyInitializationException")) {
            /* A deleted entity not having its collection initialized
             * leads to a LIE because the collection is no longer attached to the session
             *
             * But that's ok as the collection update event has been processed before
             * or the fk would have been cleared and thus triggering the cleaning
             */
            collection = null;
          }
        }
        if (collection != null) {
          for (Object collectionValue : collection) {
            processSingleContainedInInstance(
                workPlan, collectionValue, recursionContext, tenantIdentifier);
          }
        }
      } else {
        processSingleContainedInInstance(workPlan, value, recursionContext, tenantIdentifier);
      }
    }
  }
  private ContainedInRecursionContext updateContainedInRecursionContext(
      Object containedInstance,
      ContainedInMetadata containedInMetadata,
      ContainedInRecursionContext containedContext) {
    int maxDepth;
    int depth;

    // Handle @IndexedEmbedded.depth-induced limits

    Integer metadataMaxDepth = containedInMetadata.getMaxDepth();
    if (containedInstance != null && metadataMaxDepth != null) {
      maxDepth = metadataMaxDepth;
    } else {
      maxDepth = containedContext != null ? containedContext.getMaxDepth() : Integer.MAX_VALUE;
    }

    depth = containedContext != null ? containedContext.getDepth() : 0;
    if (depth < Integer.MAX_VALUE) { // Avoid integer overflow
      ++depth;
    }

    /*
     * Handle @IndexedEmbedded.includePaths-induced limits If the context for the contained element has a
     * comprehensive set of included paths, and if the @IndexedEmbedded matching the @ContainedIn we're currently
     * processing also has a comprehensive set of embedded paths, *then* we can compute the resulting set of
     * embedded fields (which is the intersection of those two sets). If this resulting set is empty, we can safely
     * stop the @ContainedIn processing: any changed field wouldn't be included in the Lucene document for
     * "containerInstance" anyway.
     */

    Set<String> comprehensivePaths;
    Set<String> metadataIncludePaths = containedInMetadata.getIncludePaths();

    /*
     * See @IndexedEmbedded.depth: it should be considered as zero if it has its default value and if includePaths
     * contains elements
     */
    if (metadataIncludePaths != null
        && !metadataIncludePaths.isEmpty()
        && metadataMaxDepth != null
        && metadataMaxDepth.equals(Integer.MAX_VALUE)) {
      String metadataPrefix = containedInMetadata.getPrefix();

      /*
       * If the contained context Filter by contained context's included paths if they are comprehensive This
       * allows to detect when a @ContainedIn is irrelevant because the matching @IndexedEmbedded would not
       * capture any property.
       */
      Set<String> containedComprehensivePaths =
          containedContext != null ? containedContext.getComprehensivePaths() : null;

      comprehensivePaths = new HashSet<>();
      for (String includedPath : metadataIncludePaths) {
        /*
         * If the contained context has a comprehensive list of included paths, use it to filter out our own
         * list
         */
        if (containedComprehensivePaths == null
            || containedComprehensivePaths.contains(includedPath)) {
          comprehensivePaths.add(metadataPrefix + includedPath);
        }
      }
    } else {
      comprehensivePaths = null;
    }

    return new ContainedInRecursionContext(maxDepth, depth, comprehensivePaths);
  }