View Javadoc
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.common;
8   
9   import ch.colabproject.colab.api.model.WithIndex;
10  import ch.colabproject.colab.generator.model.exceptions.HttpErrorMessage;
11  import ch.colabproject.colab.generator.model.exceptions.MessageI18nKey;
12  import ch.colabproject.colab.generator.model.interfaces.WithId;
13  import java.util.ArrayList;
14  import java.util.Collection;
15  import java.util.Comparator;
16  import java.util.List;
17  import javax.ejb.LocalBean;
18  import javax.ejb.Stateless;
19  import org.apache.commons.collections4.CollectionUtils;
20  
21  /**
22   * Deal with changing index in a collection. The indexes are from 1 to ..., usually with a step of 1
23   * between two items. There can be more than an increment of 1 between two items : it means there is
24   * one (or more) empty slot(s) between the items.
25   *
26   * @author sandra
27   *
28   * @param <T> Type of the items
29   */
30  // TODO smart deal with empty slots addition / moving
31  @Stateless
32  @LocalBean
33  public class IndexWithEmptySlotManager<T extends WithIndex & WithId> {
34  
35      /**
36       * The default value of the biggest index that is suitable
37       */
38      private static final int DEFAULT_MAX_INDEX = Integer.MAX_VALUE;
39  
40      /** sort by id */
41      private final Comparator<T> idComparator = Comparator
42          .comparingLong(entity -> entity.getId());
43  
44      /** sort by index - if null or 0, at the end */
45      private final Comparator<T> indexComparator = Comparator
46          .comparingLong(entity -> entity.getIndex() != 0 ? entity.getIndex() : DEFAULT_MAX_INDEX);
47  
48      // *********************************************************************************************
49      // change the position of an item
50      // *********************************************************************************************
51  
52      /**
53       * Set the new position of the item. If needed, initialize the indexes of the collection first
54       * to ensure that the indexes are unique inside the collection.
55       * <p>
56       * If we move the item where there is already a card, all cards between the old and the new
57       * position are moved to let space.
58       * <p>
59       * If we move the item to an empty slot, it is just a swap between the card and the empty place.
60       * No other card change.
61       *
62       * @param itemToMove the item to change
63       * @param newIndex   the new index to set
64       * @param collection the collection of items
65       */
66      public void changeItemPosition(T itemToMove, int newIndex, Collection<T> collection) {
67          if (CollectionUtils.isEmpty(collection)) {
68              throw HttpErrorMessage.dataError(MessageI18nKey.DATA_INTEGRITY_FAILURE);
69          }
70  
71          if (!collection.contains(itemToMove)) {
72              throw HttpErrorMessage.dataError(MessageI18nKey.DATA_INTEGRITY_FAILURE);
73          }
74  
75          List<T> sortedList = sort(collection);
76  
77          harmonizeIndexes(sortedList);
78  
79          int oldIndex = itemToMove.getIndex();
80  
81          if (oldIndex != newIndex) {
82              boolean isMovingToEmptySlot = sortedList.stream()
83                  .filter(anItem -> anItem.getIndex() == newIndex).findAny().isEmpty();
84  
85              if (isMovingToEmptySlot) {
86                  // just a swap between the card and the blank
87                  // as soon as we have a way to add an empty slot where we want, we can change this
88                  // behaviour
89                  itemToMove.setIndex(newIndex);
90              } else {
91                  List<Integer> fixedEmptySlots = listEmptySlots(sortedList);
92  
93                  boolean isMovingToBiggerIndex = newIndex > oldIndex;
94  
95                  // move items between old and new index
96                  if (isMovingToBiggerIndex) {
97                      for (T anItem : sortedList) {
98                          if (anItem.getIndex() > oldIndex && anItem.getIndex() <= newIndex) {
99                              int anItemIndex = anItem.getIndex() - 1;
100 
101                             while (fixedEmptySlots.contains(anItemIndex)) {
102                                 anItemIndex--;
103                             }
104                             anItem.setIndex(anItemIndex);
105                         }
106                     }
107                 } else {
108                     for (T anItem : sortedList) {
109                         if (anItem.getIndex() >= newIndex && anItem.getIndex() < oldIndex) {
110                             int anItemIndex = anItem.getIndex() + 1;
111 
112                             while (fixedEmptySlots.contains(anItemIndex)) {
113                                 anItemIndex++;
114                             }
115                             anItem.setIndex(anItemIndex);
116                         }
117                     }
118                 }
119 
120                 itemToMove.setIndex(newIndex);
121             }
122         }
123     }
124 
125     // *********************************************************************************************
126     // sort
127     // *********************************************************************************************
128 
129     /**
130      * Sort by index and id.
131      *
132      * @param collection the collection to sort
133      *
134      * @return an ordered by index and id list
135      */
136     private List<T> sort(Collection<T> collection) {
137         List<T> sortedList = new ArrayList<>(collection);
138 
139         sortedList.sort(idComparator);
140 
141         sortedList.sort(indexComparator);
142 
143         return sortedList;
144     }
145 
146     // *********************************************************************************************
147     // prepare
148     // *********************************************************************************************
149 
150     /**
151      * Make sure that
152      * <ul>
153      * <li>no index is 0</li>
154      * <li>each index is unique</li>
155      * </ul>
156      *
157      * @param sortedList
158      */
159     private void harmonizeIndexes(List<T> sortedList) {
160         int lastIndex = 0;
161 
162         // a priori, this simple algorithm is functional enough
163 
164         for (T item : sortedList) {
165             if (item.getIndex() <= lastIndex) {
166                 item.setIndex(lastIndex + 1);
167             }
168 
169             lastIndex = item.getIndex();
170         }
171     }
172 
173     // *********************************************************************************************
174     // deal with empty slots
175     // *********************************************************************************************
176 
177     /**
178      * List the indexes where there is an empty slot.
179      *
180      * @param sortedList
181      *
182      * @return the indexes of the empty slots
183      */
184     private List<Integer> listEmptySlots(List<T> sortedList) {
185         List<Integer> emptySlots = new ArrayList<>();
186 
187         int lastIndex = 0;
188 
189         for (T item : sortedList) {
190             lastIndex++;
191 
192             while (lastIndex < item.getIndex()) {
193                 emptySlots.add(lastIndex);
194                 lastIndex++;
195             }
196         }
197 
198         return emptySlots;
199     }
200 
201 }