001/* 002 * Copyright 2018 Anyware Services 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016package org.ametys.plugins.repository.data.holder.group.impl; 017 018import java.io.IOException; 019import java.util.Collections; 020import java.util.HashMap; 021import java.util.List; 022import java.util.Map; 023import java.util.Set; 024import java.util.TreeSet; 025 026import org.ametys.plugins.repository.RepositoryConstants; 027import org.ametys.plugins.repository.data.UnknownDataException; 028import org.ametys.plugins.repository.data.ametysobject.DataAwareAmetysObject; 029import org.ametys.plugins.repository.data.holder.DataHolder; 030import org.ametys.plugins.repository.data.holder.ModifiableModelAwareDataHolder; 031import org.ametys.plugins.repository.data.holder.group.ModifiableRepeater; 032import org.ametys.plugins.repository.data.holder.group.Repeater; 033import org.ametys.plugins.repository.data.holder.values.SynchronizableRepeater; 034import org.ametys.plugins.repository.data.holder.values.SynchronizationContext; 035import org.ametys.plugins.repository.data.holder.values.SynchronizationResult; 036import org.ametys.plugins.repository.data.repositorydata.ModifiableRepositoryData; 037import org.ametys.plugins.repository.model.RepeaterDefinition; 038import org.ametys.runtime.model.ViewItemContainer; 039import org.ametys.runtime.model.exception.BadItemTypeException; 040import org.ametys.runtime.model.exception.UndefinedItemPathException; 041 042/** 043 * CLass for modifiable model aware repeaters 044 */ 045public class ModifiableModelAwareRepeater extends ModelAwareRepeater implements ModifiableRepeater 046{ 047 private static final String __ENTRIES_TEMPORARY_NAME_PREFIX = "temp_"; 048 049 /** Modifiable repository data to use to store entries in the repository */ 050 protected ModifiableRepositoryData _modifiableRepositoryData; 051 052 /** Parent of the current {@link Repeater} */ 053 protected ModifiableModelAwareDataHolder _modifiableParent; 054 055 /** Root {@link DataHolder} */ 056 protected ModifiableModelAwareDataHolder _modifiableRoot; 057 058 /** 059 * Creates a modifiable model aware repeater 060 * @param repositoryData the repository data of the repeater 061 * @param parent the parent of the created {@link Repeater} 062 * @param root the root {@link DataAwareAmetysObject} 063 * @param definition the definition of the repeater 064 */ 065 public ModifiableModelAwareRepeater(ModifiableRepositoryData repositoryData, ModifiableModelAwareDataHolder parent, ModifiableModelAwareDataHolder root, RepeaterDefinition definition) 066 { 067 super(repositoryData, parent, root, definition); 068 _modifiableRepositoryData = repositoryData; 069 _modifiableParent = parent; 070 _modifiableRoot = root; 071 } 072 073 @SuppressWarnings("unchecked") 074 @Override 075 public List<? extends ModifiableModelAwareRepeaterEntry> getEntries() 076 { 077 return (List< ? extends ModifiableModelAwareRepeaterEntry>) super.getEntries(); 078 } 079 080 @Override 081 public ModifiableModelAwareRepeaterEntry getEntry(int position) 082 { 083 if (1 <= position && position <= getSize()) 084 { 085 ModifiableRepositoryData entryRepositoryData = _modifiableRepositoryData.getRepositoryData(String.valueOf(position)); 086 return new ModifiableModelAwareRepeaterEntry(entryRepositoryData, this, _definition); 087 } 088 else if (-getSize() < position && position <= 0) 089 { 090 // Find the positive equivalent position and call the getEntry method with this position 091 return getEntry(getSize() + position); 092 } 093 else 094 { 095 return null; 096 } 097 } 098 099 public ModifiableModelAwareRepeaterEntry addEntry() 100 { 101 return addEntry(getSize() + 1); 102 } 103 104 public ModifiableModelAwareRepeaterEntry addEntry(int position) throws IllegalArgumentException 105 { 106 if (1 <= position && position <= getSize() + 1) 107 { 108 // rename all entries that will be after the one that will be inserted 109 for (int currentEntryPosition = getSize(); currentEntryPosition >= position; currentEntryPosition--) 110 { 111 ModifiableRepositoryData entryRepositoryData = _modifiableRepositoryData.getRepositoryData(String.valueOf(currentEntryPosition)); 112 entryRepositoryData.rename(String.valueOf(currentEntryPosition + 1)); 113 } 114 115 // Add the entry at the right position 116 ModifiableRepositoryData createdRepositoryData = _modifiableRepositoryData.addRepositoryData(String.valueOf(position), RepositoryConstants.COMPOSITE_NODETYPE); 117 return new ModifiableModelAwareRepeaterEntry(createdRepositoryData, this, _definition); 118 } 119 else if (-getSize() <= position && position <= 0) 120 { 121 // Find the positive equivalent position and call the addEntry method with this position 122 return addEntry(getSize() + position + 1); 123 } 124 else 125 { 126 throw new IllegalArgumentException("The repeater named '" + _modifiableRepositoryData.getName() + "' has '" + getSize() + "' entries. You can not create an entry at position '" + position + "'."); 127 } 128 } 129 130 /** 131 * Synchronizes the given values with each repeater's entry 132 * @param <T> the type of the {@link SynchronizationResult} 133 * @param viewItemContainer The {@link ViewItemContainer} containing all items to synchronize 134 * @param repeaterValues the values of the repeater to synchronize 135 * @return the {@link SynchronizationResult} 136 * @throws UndefinedItemPathException if an entry's key refers to a data that is not defined by the model 137 * @throws BadItemTypeException if the type defined by the model of one entry's key doesn't match the corresponding value 138 * @throws IOException if an error occurs while synchronizing I/O data 139 */ 140 public <T extends SynchronizationResult> T synchronizeValues(ViewItemContainer viewItemContainer, SynchronizableRepeater repeaterValues) throws UndefinedItemPathException, BadItemTypeException, IOException 141 { 142 return synchronizeValues(viewItemContainer, repeaterValues, SynchronizationContext.newInstance()); 143 } 144 145 /** 146 * Synchronizes the given values with each repeater's entry 147 * @param <T> the type of the {@link SynchronizationResult} 148 * @param viewItemContainer The {@link ViewItemContainer} containing all items to synchronize 149 * @param repeaterValues the values of the repeater to synchronize 150 * @param context the context of the synchronization 151 * @return the {@link SynchronizationResult} 152 * @throws UndefinedItemPathException if an entry's key refers to a data that is not defined by the model 153 * @throws BadItemTypeException if the type defined by the model of one entry's key doesn't match the corresponding value 154 * @throws IOException if an error occurs while synchronizing I/O data 155 */ 156 @SuppressWarnings("unchecked") 157 public <T extends SynchronizationResult> T synchronizeValues(ViewItemContainer viewItemContainer, SynchronizableRepeater repeaterValues, SynchronizationContext context) throws UndefinedItemPathException, BadItemTypeException, IOException 158 { 159 SynchronizationResult result = new SynchronizationResult(); 160 161 if (SynchronizableRepeater.Mode.APPEND.equals(repeaterValues.getMode())) 162 { 163 if (!repeaterValues.getRemovedEntries().isEmpty()) 164 { 165 removeEntries(repeaterValues.getRemovedEntries()); 166 result.setHasChanged(true); 167 } 168 169 for (Map<String, Object> entryValues : repeaterValues.getEntries()) 170 { 171 ModifiableModelAwareRepeaterEntry entry = addEntry(); 172 SynchronizationResult entryResult = entry.synchronizeValues(viewItemContainer, entryValues, context); 173 result.aggregateResult(entryResult); 174 } 175 } 176 else if (SynchronizableRepeater.Mode.REPLACE.equals(repeaterValues.getMode())) 177 { 178 List<Integer> positions = repeaterValues.getReplacePositions(); 179 List<Map<String, Object>> entriesValues = repeaterValues.getEntries(); 180 181 for (int i = 0; i < positions.size(); i++) 182 { 183 int position = positions.get(i); 184 ModifiableModelAwareRepeaterEntry repeaterEntry = getEntry(position); 185 SynchronizationResult entryResult = repeaterEntry.synchronizeValues(viewItemContainer, entriesValues.get(i), context); 186 result.aggregateResult(entryResult); 187 } 188 } 189 else 190 { 191 List<Map<String, Object>> entriesValues = repeaterValues.getEntries(); 192 boolean moved = moveEntries(repeaterValues.getPositionsMapping(), entriesValues.size()); 193 result.setHasChanged(moved); 194 195 for (ModifiableModelAwareRepeaterEntry repeaterEntry : getEntries()) 196 { 197 int entryIndex = repeaterEntry.getPosition() - 1; 198 Map<String, Object> entryValues = entriesValues.get(entryIndex); 199 SynchronizationResult entryResult = repeaterEntry.synchronizeValues(viewItemContainer, entryValues, context); 200 result.aggregateResult(entryResult); 201 } 202 } 203 204 return (T) result; 205 } 206 207 public boolean moveEntries(Map<Integer, Integer> positionsMapping, int targetSize) 208 { 209 // For each entry, remove it if it does not appear in the mapping, or rename it with a temporary name 210 Map<Integer, String> temporaryNamesIndexedByPreviousPosition = new HashMap<>(); 211 int initialSize = getSize(); 212 boolean hasChanged = false; 213 for (int position = 1; position <= initialSize; position++) 214 { 215 if (positionsMapping.containsKey(position)) 216 { 217 if (!positionsMapping.get(position).equals(position)) 218 { 219 // Give a temporary name to the entry 220 String entryNewName = __ENTRIES_TEMPORARY_NAME_PREFIX + position; 221 ModifiableRepositoryData entryData = _modifiableRepositoryData.getRepositoryData(String.valueOf(position)); 222 entryData.rename(entryNewName); 223 temporaryNamesIndexedByPreviousPosition.put(position, entryNewName); 224 hasChanged = true; 225 } 226 } 227 else 228 { 229 // If the entry does not appear in the mapping, remove it 230 _modifiableRepositoryData.removeValue(String.valueOf(position)); 231 hasChanged = true; 232 } 233 } 234 235 // Rename all existent entries with the new position 236 for (Integer previousPosition : temporaryNamesIndexedByPreviousPosition.keySet()) 237 { 238 String temporaryName = temporaryNamesIndexedByPreviousPosition.get(previousPosition); 239 ModifiableRepositoryData entryData = _modifiableRepositoryData.getRepositoryData(temporaryName); 240 241 Integer newPosition = positionsMapping.get(previousPosition); 242 entryData.rename(String.valueOf(newPosition)); 243 } 244 245 // Add the new entries to fill holes until the target size is reached 246 for (int position = 1; position <= targetSize; position++) 247 { 248 if (!_modifiableRepositoryData.hasValue(String.valueOf(position))) 249 { 250 _modifiableRepositoryData.addRepositoryData(String.valueOf(position), RepositoryConstants.COMPOSITE_NODETYPE); 251 hasChanged = true; 252 } 253 } 254 255 return hasChanged; 256 } 257 258 public void removeEntries(Set<Integer> positions) throws UnknownDataException 259 { 260 // Create a list to sort the entries positions and remove from the last one to the first one 261 Set<Integer> sortedPositions = new TreeSet<>(Collections.reverseOrder()); 262 for (Integer position : positions) 263 { 264 if (1 <= position && position <= getSize()) 265 { 266 sortedPositions.add(position); 267 } 268 else if (-getSize() < position && position <= 0) 269 { 270 // Find the positive equivalent position 271 Integer equivalentPosition = getSize() + position; 272 sortedPositions.add(equivalentPosition); 273 } 274 else 275 { 276 throw new UnknownDataException("Unable to remove the entry at position '" + position + "' because there is no entry at this position."); 277 } 278 } 279 280 sortedPositions.stream() 281 .forEach(position -> this.removeEntry(position)); 282 } 283 284 public void removeEntry(int position) throws UnknownDataException 285 { 286 if (1 <= position && position <= getSize()) 287 { 288 // remove the entry 289 _modifiableRepositoryData.removeValue(String.valueOf(position)); 290 291 // rename all entries after the removed one 292 for (int currentEntryPosition = position + 1; currentEntryPosition <= getSize() + 1; currentEntryPosition++) 293 { 294 ModifiableRepositoryData entryRepositoryData = _modifiableRepositoryData.getRepositoryData(String.valueOf(currentEntryPosition)); 295 entryRepositoryData.rename(String.valueOf(currentEntryPosition - 1)); 296 } 297 } 298 else if (-getSize() < position && position <= 0) 299 { 300 // Find the positive equivalent position and call the removeEntry method with this position 301 removeEntry(getSize() + position); 302 } 303 else 304 { 305 throw new UnknownDataException("Unable to remove the entry at position '" + position + "' because there is no entry at this position."); 306 } 307 } 308 309 @Override 310 public ModifiableRepositoryData getRepositoryData() 311 { 312 return _modifiableRepositoryData; 313 } 314 315 @Override 316 public ModifiableModelAwareDataHolder getParentDataHolder() 317 { 318 return _modifiableParent; 319 } 320 321 @Override 322 public ModifiableModelAwareDataHolder getRootDataHolder() 323 { 324 return _modifiableRoot; 325 } 326}