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 }