IdentifiableUtils.java

package com.albertsilva.dev.asjcatalog.util;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import com.albertsilva.dev.asjcatalog.domain.Identifiable;

/**
 * Utilitário estático para trabalhar com objetos que possuem identificador
 * ({@link Identifiable}).
 *
 * <p>
 * Contém um único método, {@link #reorderByReference(List, List)}, que
 * reordena uma lista de entidades seguindo a ordem de uma segunda lista de
 * referência. No backend ele existe para um caso específico:
 * {@code ProductService.findAllPaged} obtém primeiro a página de produtos
 * (ids e nomes, já na ordem pedida pelo cliente) e depois carrega as entidades
 * com as categorias em uma consulta {@code IN}, cuja ordem de retorno não é
 * garantida; este método restaura a ordem da página.
 * </p>
 *
 * <p>
 * A classe é {@code final}, não guarda estado e só tem métodos estáticos. Ela
 * <b>não declara construtor privado</b>, portanto o construtor padrão público
 * continua disponível (a classe não impede ser instanciada).
 * </p>
 */
public final class IdentifiableUtils {

  /**
   * Reordena {@code unordered} segundo a ordem dos identificadores de
   * {@code ordered}.
   *
   * <p>
   * Funcionamento: indexa os elementos de {@code unordered} por
   * {@code getId()} e, em seguida, percorre {@code ordered}; para cada
   * identificador encontrado no índice, acrescenta o elemento correspondente ao
   * resultado. A ordem final é, portanto, a de {@code ordered}.
   * </p>
   *
   * <p>
   * Comportamento a conhecer:
   * </p>
   * <ul>
   * <li>Elementos de {@code ordered} sem correspondente em {@code unordered} são
   * <b>descartados</b> em silêncio, assim como elementos de {@code unordered}
   * cujo id não aparece em {@code ordered}; o resultado pode ter menos elementos
   * que qualquer uma das listas.</li>
   * <li>Se {@code unordered} tiver dois elementos com o mesmo id, prevalece o
   * <b>último</b>; se {@code ordered} repetir um id, o elemento correspondente
   * aparece repetido no resultado.</li>
   * <li>Os ids são comparados com {@code equals}/{@code hashCode} do tipo
   * {@code ID}. Um id {@code null} é aceito como chave e pode ser
   * correspondido.</li>
   * <li>As listas recebidas não são modificadas; o retorno é uma nova
   * {@link ArrayList} (mutável), com custo proporcional ao tamanho das duas
   * listas.</li>
   * <li>Não sincroniza nada: é seguro para uso concorrente desde que as listas
   * recebidas não sejam alteradas por outra thread durante a chamada.</li>
   * </ul>
   *
   * @param unordered lista de elementos a reordenar (não pode ser {@code null}
   *                  nem conter elementos {@code null})
   * @param ordered   lista que define a ordem desejada, por identificador
   *                  (não pode ser {@code null} nem conter elementos
   *                  {@code null}); pode ser de outro tipo, desde que seja
   *                  {@link Identifiable} com o mesmo {@code ID}
   * @param <ID>      tipo do identificador
   * @param <T>       tipo dos elementos a reordenar
   * @return nova lista com os elementos de {@code unordered} na ordem de
   *         {@code ordered}
   * @throws NullPointerException se alguma das listas ou algum de seus elementos
   *                              for {@code null}
   */
  public static <ID, T extends Identifiable<ID>> List<T> reorderByReference(List<T> unordered,
      List<? extends Identifiable<ID>> ordered) {

    Map<ID, T> map = new HashMap<>();

    for (T entity : unordered) {
      map.put(entity.getId(), entity);
    }

    List<T> result = new ArrayList<>();

    for (Identifiable<ID> identifiable : ordered) {
      T matched = map.get(identifiable.getId());

      if (matched != null) {
        result.add(matched);
      }
    }

    return result;
  }

}