IndexGeneratorHelper.java

  1. /*
  2.  * The coLAB project
  3.  * Copyright (C) 2021-2023 AlbaSim, MEI, HEIG-VD, HES-SO
  4.  *
  5.  * Licensed under the MIT License
  6.  */
  7. package ch.colabproject.colab.api.controller.document;

  8. import ch.colabproject.colab.api.model.WithIndex;
  9. import ch.colabproject.colab.generator.model.exceptions.HttpErrorMessage;
  10. import ch.colabproject.colab.generator.model.exceptions.MessageI18nKey;
  11. import java.util.ArrayList;
  12. import java.util.Collection;
  13. import java.util.Comparator;
  14. import java.util.List;
  15. import javax.ejb.LocalBean;
  16. import javax.ejb.Stateless;

  17. /**
  18.  * Deal with assigning index in a collection. The index is a field of the object and not the
  19.  * built-in index of a list.
  20.  * <p>
  21.  * The indexes are progressive (from small to big) but the size of the room between two neighboring
  22.  * indexes is not minimal nor has to be regular. In other words two neighboring indexes do not need
  23.  * to be two just-following numbers.
  24.  * <p>
  25.  * The purpose is to be able to easily add an object between others without having to adjust other
  26.  * indexes. In order to do that, we set a room between two neighboring indexes.
  27.  *
  28.  * @author sandra
  29.  * @author maxence
  30.  *
  31.  * @param <T> Type of the items
  32.  */
  33. @Stateless
  34. @LocalBean
  35. public class IndexGeneratorHelper<T extends WithIndex> {

  36.     /**
  37.      * The default value of the smallest index that is suitable
  38.      */
  39.     private final int DEFAULT_MIN_INDEX = 0;

  40.     /**
  41.      * The default value of the biggest index that is suitable
  42.      */
  43.     private final int DEFAULT_MAX_INDEX = Integer.MAX_VALUE;

  44.     /**
  45.      * Default room between two neighboring indexes
  46.      */
  47.     private final int DEFAULT_INDEX_INC = 1000;

  48.     /**
  49.      * Value of the smallest index that is suitable
  50.      */
  51.     private final int minIndex;

  52.     /**
  53.      * Value of the biggest index that is suitable
  54.      */
  55.     private final int maxIndex;

  56.     /**
  57.      * Initial room between two neighboring indexes
  58.      */
  59.     private final int indexIncrement;

  60.     /**
  61.      * Index for the first item in the list
  62.      */
  63.     private final int soloIndex;

  64.     /**
  65.      * Default constructor
  66.      */
  67.     public IndexGeneratorHelper() {
  68.         minIndex = DEFAULT_MIN_INDEX;
  69.         maxIndex = DEFAULT_MAX_INDEX;
  70.         indexIncrement = DEFAULT_INDEX_INC;

  71.         soloIndex = minIndex + indexIncrement;
  72.     }

  73.     /**
  74.      * Constructor with parameters
  75.      *
  76.      * @param minIndex       The value of the smallest index that is suitable
  77.      * @param maxIndex       The value of the biggest index that is suitable
  78.      * @param indexIncrement The initial room between two neighboring indexes
  79.      */
  80.     public IndexGeneratorHelper(int minIndex, int maxIndex, int indexIncrement) {
  81.         this.minIndex = minIndex;
  82.         this.maxIndex = maxIndex;
  83.         this.indexIncrement = indexIncrement;

  84.         soloIndex = minIndex + indexIncrement;
  85.     }

  86.     // *********************************************************************************************
  87.     //
  88.     // *********************************************************************************************

  89.     /**
  90.      * Move (or set) the given item at the beginning of the collection.
  91.      * <p>
  92.      * That means setting an index to the item so that it is the first when sorting by index.
  93.      *
  94.      * @param item       the item waiting for an index to be set
  95.      * @param collection the collection of items
  96.      *
  97.      * @throws HttpErrorMessage if the collection is too big
  98.      */
  99.     public void moveItemToBeginning(T item, Collection<T> collection) {
  100.         if (collection.isEmpty()) {
  101.             item.setIndex(soloIndex);
  102.         } else {
  103.             List<T> sortedList = sortCollection(collection);

  104.             T baseItem = sortedList.get(0);

  105.             int justTooLow = minIndex - 1;

  106.             if (!hasEnoughSpaceBetween(justTooLow, baseItem.getIndex())) {
  107.                 reorderIndexes(sortedList);
  108.             }

  109.             int wantedIndex = computeInBetweenIndex(justTooLow, baseItem.getIndex());

  110.             item.setIndex(wantedIndex);
  111.         }
  112.     }

  113.     /**
  114.      * Move the given item one step ahead from where it is now.
  115.      *
  116.      * @param item       the item waiting for a new index
  117.      * @param collection the collection of items
  118.      *
  119.      * @throws HttpErrorMessage if the item is not in the collection
  120.      */
  121.     public void moveOneStepAhead(T item, Collection<T> collection) {
  122.         if (!collection.contains(item)) {
  123.             throw HttpErrorMessage.dataError(MessageI18nKey.DATA_INTEGRITY_FAILURE);
  124.         }

  125.         List<T> sortedList = sortCollection(collection);

  126.         T item1 = getItemBefore(item, sortedList);

  127.         if (item1 == null) {
  128.             // already at the beginning
  129.             return;
  130.         }

  131.         int itemIndex = item.getIndex();
  132.         item.setIndex(item1.getIndex());
  133.         item1.setIndex(itemIndex);
  134.     }

  135.     /**
  136.      * Move (or set) the given item before the baseItem.
  137.      *
  138.      * @param item       the item waiting for a new index
  139.      * @param baseItem   the item of reference
  140.      * @param collection the collection of items
  141.      *
  142.      * @throws HttpErrorMessage if the baseItem is not in the collection or if the collection is too
  143.      *                          big
  144.      */
  145.     public void moveItemBefore(T item, T baseItem, Collection<T> collection) {
  146.         if (!collection.contains(baseItem)) {
  147.             throw HttpErrorMessage.dataError(MessageI18nKey.DATA_INTEGRITY_FAILURE);
  148.         }

  149.         List<T> sortedList = sortCollection(collection);

  150.         T item1 = baseItem;
  151.         T item2 = getItemBefore(baseItem, sortedList);

  152.         if (item2 == null) {
  153.             // set before item1 which is the first item
  154.             moveItemToBeginning(item, collection);

  155.         } else {
  156.             // between two items
  157.             if (!hasEnoughSpaceBetween(item1.getIndex(), item2.getIndex())) {
  158.                 reorderIndexes(sortedList);
  159.             }

  160.             int wantedIndex = computeInBetweenIndex(item1.getIndex(), item2.getIndex());

  161.             item.setIndex(wantedIndex);
  162.         }
  163.     }

  164.     /**
  165.      * Move an item one step behind from where it is now.
  166.      *
  167.      * @param item       the item waiting for a new index
  168.      * @param collection the collection of items
  169.      *
  170.      * @throws HttpErrorMessage if the item is not in the collection
  171.      */
  172.     public void moveOneStepBehind(T item, Collection<T> collection) {
  173.         if (!collection.contains(item)) {
  174.             throw HttpErrorMessage.dataError(MessageI18nKey.DATA_INTEGRITY_FAILURE);
  175.         }

  176.         List<T> sortedList = sortCollection(collection);

  177.         T item1 = getItemAfter(item, sortedList);

  178.         if (item1 == null) {
  179.             // already at the end
  180.             return;
  181.         }

  182.         int itemIndex = item.getIndex();
  183.         item.setIndex(item1.getIndex());
  184.         item1.setIndex(itemIndex);
  185.     }

  186.     /**
  187.      * Move (or set) the given item after the baseItem.
  188.      *
  189.      * @param item       the item waiting for a new index
  190.      * @param baseItem   the item of reference
  191.      * @param collection the collection of items
  192.      *
  193.      * @throws HttpErrorMessage if the baseItem is not in the collection or if the collection is too
  194.      *                          big
  195.      */
  196.     public void moveItemAfter(T item, T baseItem, Collection<T> collection) {
  197.         if (!collection.contains(baseItem)) {
  198.             throw HttpErrorMessage.dataError(MessageI18nKey.DATA_INTEGRITY_FAILURE);
  199.         }

  200.         List<T> sortedList = sortCollection(collection);

  201.         T item1 = baseItem;
  202.         T item2 = getItemAfter(baseItem, sortedList);

  203.         if (item2 == null) {
  204.             // set after item1 which is the last item
  205.             moveItemToEnd(item, collection);

  206.         } else {
  207.             if (!hasEnoughSpaceBetween(item1.getIndex(), item2.getIndex())) {
  208.                 reorderIndexes(sortedList);
  209.             }

  210.             int wantedIndex = computeInBetweenIndex(item1.getIndex(), item2.getIndex());

  211.             item.setIndex(wantedIndex);
  212.         }
  213.     }

  214.     /**
  215.      * Move (or set) the given item at the end of the collection.
  216.      * <p>
  217.      * That means setting an index to the item so that it is the last when sorting by index.
  218.      *
  219.      * @param item       the item waiting for an index to be set
  220.      * @param collection the collection of items
  221.      *
  222.      * @throws HttpErrorMessage if the collection is too big
  223.      */
  224.     public void moveItemToEnd(T item, Collection<T> collection) {
  225.         if (collection.isEmpty()) {
  226.             item.setIndex(soloIndex);
  227.         } else {
  228.             List<T> sortedList = sortCollection(collection);

  229.             T baseItem = sortedList.get(sortedList.size() - 1);

  230.             int justTooBigIndex = maxIndex + 1;

  231.             if (!hasEnoughSpaceBetween(baseItem.getIndex(), justTooBigIndex)) {
  232.                 reorderIndexes(sortedList);
  233.             }

  234.             int wantedIndex;
  235.             if ((justTooBigIndex - baseItem.getIndex()) > indexIncrement) {
  236.                 wantedIndex = baseItem.getIndex() + indexIncrement;
  237.             } else {
  238.                 wantedIndex = computeInBetweenIndex(baseItem.getIndex(), justTooBigIndex);
  239.             }

  240.             item.setIndex(wantedIndex);
  241.         }
  242.     }

  243.     // *********************************************************************************************
  244.     //
  245.     // *********************************************************************************************

  246.     /**
  247.      * Reorder the list so that each neighboring items have items separated by
  248.      * {@link #indexIncrement}.
  249.      * <p>
  250.      * The first item also has a room of {@link #indexIncrement} before it.
  251.      *
  252.      * @param sortedCollection The sorted collection to index again
  253.      *
  254.      * @throws HttpErrorMessage if the collection is too big to be reorder
  255.      */
  256.     private void reorderIndexes(List<T> sortedCollection) {
  257.         if (!canBeReordered(sortedCollection)) {
  258.             throw HttpErrorMessage.dataError(MessageI18nKey.DATA_INTEGRITY_FAILURE);
  259.         }

  260.         int index = minIndex + indexIncrement;

  261.         for (WithIndex obj : sortedCollection) {
  262.             obj.setIndex(index);
  263.             index += indexIncrement;
  264.         }
  265.     }

  266.     /**
  267.      * Compute if the collection size fits into items indexes separated by {@link #indexIncrement}
  268.      * between {@link #minIndex} and {@link #maxIndex}
  269.      *
  270.      * @param collection
  271.      *
  272.      * @return True if the collection size fits into
  273.      */
  274.     private boolean canBeReordered(Collection<T> collection) {
  275.         return Math.floor(((float) (maxIndex - minIndex)) / indexIncrement) - 1 >= collection
  276.             .size();
  277.     }

  278.     // *********************************************************************************************
  279.     //
  280.     // *********************************************************************************************

  281.     /**
  282.      * Sort the collection by the items index field.
  283.      *
  284.      * @param collection the collection of items to sort
  285.      *
  286.      * @return a list of sorted items by index
  287.      */
  288.     private List<T> sortCollection(Collection<T> collection) {
  289.         List<T> sortedList = new ArrayList<>(collection);

  290.         sortedList.sort(Comparator.comparingInt(obj -> obj.getIndex()));

  291.         // TODO sandra ask Maxence if the old way was more suitable
  292. //      sortedList.sort((a, b) -> {
  293. //      if (a != null) {
  294. //          if (b != null) {
  295. //              return Math.max(a.getIndex(), b.getIndex());
  296. //          } else {
  297. //              // a is not null, b is null
  298. //              return -1;
  299. //          }
  300. //      } else if (b != null) {
  301. //          // a is null, not b
  302. //          return 1;
  303. //      }
  304. //      //both are null
  305. //      return 0;

  306.         return sortedList;
  307.     }

  308.     // *********************************************************************************************
  309.     //
  310.     // *********************************************************************************************

  311.     /**
  312.      * Determine if there is enough space between two indexes so that we can insert something new.
  313.      *
  314.      * @param indexA an index
  315.      * @param indexB another index
  316.      *
  317.      * @return True if there is at least 1 room left between the two indexes
  318.      */
  319.     private boolean hasEnoughSpaceBetween(int indexA, int indexB) {
  320.         return Math.abs(indexB - indexA) > 1;
  321.     }

  322.     /**
  323.      * Determine the new index which will take place between the two given indexes.
  324.      *
  325.      * @param indexA an index
  326.      * @param indexB another index
  327.      *
  328.      * @return An index that fits between the two given indexes
  329.      */
  330.     private int computeInBetweenIndex(int indexA, int indexB) {
  331.         int baseIndex = indexA;

  332.         int delta = indexB - baseIndex;
  333.         int increment = (int) Math.floor(delta / 2.0);

  334.         return baseIndex + increment;
  335.     }

  336.     /**
  337.      * Fetch the item which is just before the given baseItem in the sortedList.
  338.      *
  339.      * @param baseItem   the reference item
  340.      * @param sortedList the sorted list of items
  341.      *
  342.      * @return the item just before baseItem
  343.      *
  344.      * @throws HttpErrorMessage if the sorted list does not contain the baseItem
  345.      */
  346.     private T getItemBefore(T baseItem, List<T> sortedList) {
  347.         if (!sortedList.contains(baseItem)) {
  348.             throw HttpErrorMessage.dataError(MessageI18nKey.DATA_INTEGRITY_FAILURE);
  349.         }

  350.         int baseIndexInList = sortedList.indexOf(baseItem);

  351.         if (baseIndexInList > 0) {
  352.             return sortedList.get(baseIndexInList - 1);
  353.         } else {
  354.             // nothing before baseItem
  355.             return null;
  356.         }
  357.     }

  358.     /**
  359.      * Fetch the item which is just after the given baseItem in the sortedList.
  360.      *
  361.      * @param baseItem   the reference item
  362.      * @param sortedList the sorted list of items
  363.      *
  364.      * @return the item just after baseItem
  365.      *
  366.      * @throws HttpErrorMessage if the sorted list does not contain the baseItem
  367.      */
  368.     private T getItemAfter(T baseItem, List<T> sortedList) {
  369.         if (!sortedList.contains(baseItem)) {
  370.             throw HttpErrorMessage.dataError(MessageI18nKey.DATA_INTEGRITY_FAILURE);
  371.         }

  372.         int baseIndexInList = sortedList.indexOf(baseItem);

  373.         if (baseIndexInList < sortedList.size() - 1) {
  374.             return sortedList.get(baseIndexInList + 1);
  375.         } else {
  376.             // nothing after baseItem
  377.             return null;
  378.         }
  379.     }
  380. }