PriorityAsSupport.java

/*
 * *********************************************************************************************************************
 *
 * blueMarine II: Semantic Media Centre
 * http://tidalwave.it/projects/bluemarine2
 *
 * Copyright (C) 2015 - 2021 by Tidalwave s.a.s. (http://tidalwave.it)
 *
 * *********************************************************************************************************************
 *
 * Licensed 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.
 *
 * *********************************************************************************************************************
 *
 * git clone https://bitbucket.org/tidalwave/bluemarine2-src
 * git clone https://github.com/tidalwave-it/bluemarine2-src
 *
 * *********************************************************************************************************************
 */
package it.tidalwave.util.spi;

import javax.annotation.Nonnull;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import it.tidalwave.util.As;
import it.tidalwave.util.AsException;
import it.tidalwave.dci.annotation.DciRole;
import lombok.extern.slf4j.Slf4j;

/***********************************************************************************************************************
 *
 * A specialisation of {@link AsSupport} that deals with multiple roles of the same type by prioritising them; they
 * are ordered from most relevant to least relevant (where relevance is associated to specialisation, that is most
 * specialised roles, or roles associated via {@code @DciRole} to most specialised datum classes, are most relevant).
 *
 * FIXME: could be seen as a replacement to {@code AsSupport}?
 *
 * @author  Fabrizio Giudici
 *
 **********************************************************************************************************************/
@Slf4j
public class PriorityAsSupport extends AsSupport implements As
  {
    @FunctionalInterface
    public static interface RoleProvider
      {
        @Nonnull
        public <T> Collection<T> findRoles (@Nonnull final Class<T> type);
      }

    @Nonnull
    private final Object owner; // for logging only

    @Nonnull
    private final Optional<RoleProvider> additionalRoleProvider;

    public PriorityAsSupport (final Object owner)
      {
        this(owner, Collections.emptyList());
      }

    public PriorityAsSupport (@Nonnull final Object owner, @Nonnull final Collection<Object> rolesOrFactories)
      {
        super(owner, rolesOrFactories);
        this.owner = owner;
        this.additionalRoleProvider = Optional.empty();
      }

    public PriorityAsSupport (@Nonnull final Object owner,
                              @Nonnull final RoleProvider additionalRoleProvider,
                              @Nonnull final Collection<Object> rolesOrFactories)
      {
        super(owner, rolesOrFactories);
        this.owner = owner;
        this.additionalRoleProvider = Optional.of(additionalRoleProvider);
      }

    /*******************************************************************************************************************
     *
     * {@inheritDoc}
     *
     * Returned roles can be associated both to this type and to the delegate; the one with the higher priority is
     * returned. See {@link #asMany(java.lang.Class)} for further details.
     *
     * @see #asMany(java.lang.Class)
     *
     ******************************************************************************************************************/
    @Override @Nonnull
    public <T> T as (@Nonnull final Class<T> type)
      {
        return as(type, As.Defaults.throwAsException(type));
      }

    /*******************************************************************************************************************
     *
     * {@inheritDoc}
     *
     * Returned roles can be associated both to this type and to the delegate; the one with the higher priority is
     * returned. See {@link #asMany(java.lang.Class)} for further details.
     *
     * @see #asMany(java.lang.Class)
     *
     ******************************************************************************************************************/
    @Override @Nonnull
    public <T> T as (@Nonnull final Class<T> type, @Nonnull final NotFoundBehaviour<T> notFoundBehaviour)
      {
        return maybeAs(type).orElseGet(() -> notFoundBehaviour.run(new AsException(type)));
      }

    /*******************************************************************************************************************
     *
     * {@inheritDoc}
     *
     * Returned roles can be associated both to this type and to the delegate; the one with the higher priority is
     * returned. See {@link #asMany(java.lang.Class)} for further details.
     *
     * @see #asMany(java.lang.Class)
     *
     ******************************************************************************************************************/
    @Override @Nonnull
    public <T> Optional<T> maybeAs (@Nonnull final Class<T> type)
      {
        return asMany(type).stream().findFirst();
      }

    /*******************************************************************************************************************
     *
     * {@inheritDoc}
     *
     * Returned roles can be associated both to this type and to the delegate; the one with the higher priority is
     * returned. The ones associated to this type come with higher priority (this makes sense, being this class a
     * decorator, specific roles could be associated to it). But given that the default implementation of asMany()
     * doesn't guarantee ant order (see TFT-192) there's something to take care of. Currently this method contains
     * some hardwired priority logics.
     *
     ******************************************************************************************************************/
    @Override @Nonnull
    public <T> Collection<T> asMany (@Nonnull final Class<T> type)
      {
        log.trace("asMany({}) - {}", type, owner);
        final List<T> unordered = new ArrayList<>(super.asMany(type));
        additionalRoleProvider.ifPresent(r -> unordered.addAll(r.findRoles(type)));
        //
        // Need a kind of bubble sort, because:
        // a) the original sequence might have a meaning; for instance, additional roles added by
        //    additionalRoleProvider are appended and, generally, they should stay low in priority.
        // b) there is not always a well-defined way to define a relation order between the elements.
        //
        final List<T> result = new ArrayList<>();
        unordered.forEach(item -> addInOrder(result, item));
        log.trace(">>>> returning {}", result);

        return result;
      }

    /*******************************************************************************************************************
     *
     * Adds an item to the list, just before the first existing item which whose datum class is an instance of a
     * subclass of its datum class.
     *
     ******************************************************************************************************************/
    private static <T> void addInOrder (@Nonnull final List<T> list, @Nonnull final T item)
      {
        log.trace(">>>> add in order {} into {}", item, list);
        final Optional<T> firstAncestor = list.stream().filter(i -> isDatumAncestor(i, item)).findFirst();
        final int index = firstAncestor.map(list::indexOf).orElse(list.size());
        list.add(index, item);
        log.trace(">>>>>>>> add in order {} ", list);
      }

    /*******************************************************************************************************************
     *
     ******************************************************************************************************************/
    private static <T> boolean isDatumAncestor (@Nonnull final T a, @Nonnull final T b)
      {
        final DciRole aBoundDatumClass = a.getClass().getAnnotation(DciRole.class);
        final DciRole bBoundDatumClass = b.getClass().getAnnotation(DciRole.class);

        if ((aBoundDatumClass != null) && (bBoundDatumClass != null))
          {
            return aBoundDatumClass.datumType()[0].isAssignableFrom(bBoundDatumClass.datumType()[0]); // FIXME: multiple classes?
          }

        return a.getClass().isAssignableFrom(b.getClass());
      }
  }