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.cms.data.holder.group.impl; 017 018import java.util.Collections; 019import java.util.HashMap; 020import java.util.List; 021import java.util.Map; 022import java.util.Set; 023import java.util.TreeSet; 024 025import org.ametys.cms.data.holder.ModifiableIndexableDataHolder; 026import org.ametys.cms.data.holder.group.ModifiableIndexableRepeater; 027import org.ametys.cms.data.holder.group.ModifiableIndexableRepeaterEntry; 028import org.ametys.plugins.repository.RepositoryConstants; 029import org.ametys.plugins.repository.data.UnknownDataException; 030import org.ametys.plugins.repository.data.holder.DataHolder; 031import org.ametys.plugins.repository.data.holder.group.ModifiableModelAwareRepeaterEntry; 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 DefaultModifiableModelAwareRepeater extends DefaultModelAwareRepeater implements ModifiableIndexableRepeater 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 ModifiableIndexableDataHolder _modifiableParent; 054 055 /** Root {@link DataHolder} */ 056 protected ModifiableIndexableDataHolder _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 DataHolder} 063 * @param definition the definition of the repeater 064 */ 065 public DefaultModifiableModelAwareRepeater(ModifiableRepositoryData repositoryData, ModifiableIndexableDataHolder parent, ModifiableIndexableDataHolder 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 ModifiableIndexableRepeaterEntry> getEntries() 076 { 077 return (List< ? extends ModifiableIndexableRepeaterEntry>) super.getEntries(); 078 } 079 080 @Override 081 public ModifiableIndexableRepeaterEntry getEntry(int position) 082 { 083 if (1 <= position && position <= getSize()) 084 { 085 ModifiableRepositoryData entryRepositoryData = _modifiableRepositoryData.getRepositoryData(String.valueOf(position)); 086 return new DefaultModifiableModelAwareRepeaterEntry(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 ModifiableIndexableRepeaterEntry addEntry() 100 { 101 return addEntry(getSize() + 1); 102 } 103 104 public ModifiableIndexableRepeaterEntry 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 DefaultModifiableModelAwareRepeaterEntry(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 */ 139 public <T extends SynchronizationResult> T synchronizeValues(ViewItemContainer viewItemContainer, SynchronizableRepeater repeaterValues) throws UndefinedItemPathException, BadItemTypeException 140 { 141 return synchronizeValues(viewItemContainer, repeaterValues, SynchronizationContext.newInstance()); 142 } 143 144 /** 145 * Synchronizes the given values with each repeater's entry 146 * @param <T> the type of the {@link SynchronizationResult} 147 * @param viewItemContainer The {@link ViewItemContainer} containing all items to synchronize 148 * @param repeaterValues the values of the repeater to synchronize 149 * @param context the context of the synchronization 150 * @return the {@link SynchronizationResult} 151 * @throws UndefinedItemPathException if an entry's key refers to a data that is not defined by the model 152 * @throws BadItemTypeException if the type defined by the model of one entry's key doesn't match the corresponding value 153 */ 154 @SuppressWarnings("unchecked") 155 public <T extends SynchronizationResult> T synchronizeValues(ViewItemContainer viewItemContainer, SynchronizableRepeater repeaterValues, SynchronizationContext context) throws UndefinedItemPathException, BadItemTypeException 156 { 157 SynchronizationResult result = new SynchronizationResult(); 158 159 if (SynchronizableRepeater.Mode.APPEND.equals(repeaterValues.getMode())) 160 { 161 if (!repeaterValues.getRemovedEntries().isEmpty()) 162 { 163 removeEntries(repeaterValues.getRemovedEntries()); 164 result.setHasChanged(true); 165 } 166 167 for (Map<String, Object> entryValues : repeaterValues.getEntries()) 168 { 169 ModifiableModelAwareRepeaterEntry entry = addEntry(); 170 SynchronizationResult entryResult = entry.synchronizeValues(viewItemContainer, entryValues, context); 171 result.aggregateResult(entryResult); 172 } 173 } 174 else if (SynchronizableRepeater.Mode.REPLACE.equals(repeaterValues.getMode())) 175 { 176 List<Integer> positions = repeaterValues.getReplacePositions(); 177 List<Map<String, Object>> entriesValues = repeaterValues.getEntries(); 178 179 for (int i = 0; i < positions.size(); i++) 180 { 181 int position = positions.get(i); 182 ModifiableModelAwareRepeaterEntry repeaterEntry = getEntry(position); 183 SynchronizationResult entryResult = repeaterEntry.synchronizeValues(viewItemContainer, entriesValues.get(i), context); 184 result.aggregateResult(entryResult); 185 } 186 } 187 else 188 { 189 List<Map<String, Object>> entriesValues = repeaterValues.getEntries(); 190 boolean moved = moveEntries(repeaterValues.getPositionsMapping(), entriesValues.size()); 191 result.setHasChanged(moved); 192 193 for (ModifiableModelAwareRepeaterEntry repeaterEntry : getEntries()) 194 { 195 int entryIndex = repeaterEntry.getPosition() - 1; 196 Map<String, Object> entryValues = entriesValues.get(entryIndex); 197 SynchronizationResult entryResult = repeaterEntry.synchronizeValues(viewItemContainer, entryValues, context); 198 result.aggregateResult(entryResult); 199 } 200 } 201 202 return (T) result; 203 } 204 205 public boolean moveEntries(Map<Integer, Integer> positionsMapping, int targetSize) 206 { 207 // For each entry, remove it if it does not appear in the mapping, or rename it with a temporary name 208 Map<Integer, String> temporaryNamesIndexedByPreviousPosition = new HashMap<>(); 209 int initialSize = getSize(); 210 boolean hasChanged = false; 211 for (int position = 1; position <= initialSize; position++) 212 { 213 if (positionsMapping.containsKey(position)) 214 { 215 if (!positionsMapping.get(position).equals(position)) 216 { 217 // Give a temporary name to the entry 218 String entryNewName = __ENTRIES_TEMPORARY_NAME_PREFIX + position; 219 ModifiableRepositoryData entryData = _modifiableRepositoryData.getRepositoryData(String.valueOf(position)); 220 entryData.rename(entryNewName); 221 temporaryNamesIndexedByPreviousPosition.put(position, entryNewName); 222 hasChanged = true; 223 } 224 } 225 else 226 { 227 // If the entry does not appear in the mapping, remove it 228 _modifiableRepositoryData.removeValue(String.valueOf(position)); 229 hasChanged = true; 230 } 231 } 232 233 // Rename all existent entries with the new position 234 for (Integer previousPosition : temporaryNamesIndexedByPreviousPosition.keySet()) 235 { 236 String temporaryName = temporaryNamesIndexedByPreviousPosition.get(previousPosition); 237 ModifiableRepositoryData entryData = _modifiableRepositoryData.getRepositoryData(temporaryName); 238 239 Integer newPosition = positionsMapping.get(previousPosition); 240 entryData.rename(String.valueOf(newPosition)); 241 } 242 243 // Add the new entries to fill holes until the target size is reached 244 for (int position = 1; position <= targetSize; position++) 245 { 246 if (!_modifiableRepositoryData.hasValue(String.valueOf(position))) 247 { 248 _modifiableRepositoryData.addRepositoryData(String.valueOf(position), RepositoryConstants.COMPOSITE_NODETYPE); 249 hasChanged = true; 250 } 251 } 252 253 return hasChanged; 254 } 255 256 public void removeEntries(Set<Integer> positions) throws UnknownDataException 257 { 258 // Create a list to sort the entries positions and remove from the last one to the first one 259 Set<Integer> sortedPositions = new TreeSet<>(Collections.reverseOrder()); 260 for (Integer position : positions) 261 { 262 if (1 <= position && position <= getSize()) 263 { 264 sortedPositions.add(position); 265 } 266 else if (-getSize() < position && position <= 0) 267 { 268 // Find the positive equivalent position 269 Integer equivalentPosition = getSize() + position; 270 sortedPositions.add(equivalentPosition); 271 } 272 else 273 { 274 throw new UnknownDataException("Unable to remove the entry at position '" + position + "' because there is no entry at this position."); 275 } 276 } 277 278 sortedPositions.stream() 279 .forEach(position -> this.removeEntry(position)); 280 } 281 282 public void removeEntry(int position) throws UnknownDataException 283 { 284 if (1 <= position && position <= getSize()) 285 { 286 // remove the entry 287 _modifiableRepositoryData.removeValue(String.valueOf(position)); 288 289 // rename all entries after the removed one 290 for (int currentEntryPosition = position + 1; currentEntryPosition <= getSize() + 1; currentEntryPosition++) 291 { 292 ModifiableRepositoryData entryRepositoryData = _modifiableRepositoryData.getRepositoryData(String.valueOf(currentEntryPosition)); 293 entryRepositoryData.rename(String.valueOf(currentEntryPosition - 1)); 294 } 295 } 296 else if (-getSize() < position && position <= 0) 297 { 298 // Find the positive equivalent position and call the removeEntry method with this position 299 removeEntry(getSize() + position); 300 } 301 else 302 { 303 throw new UnknownDataException("Unable to remove the entry at position '" + position + "' because there is no entry at this position."); 304 } 305 } 306 307 @Override 308 public ModifiableRepositoryData getRepositoryData() 309 { 310 return _modifiableRepositoryData; 311 } 312 313 @Override 314 public ModifiableIndexableDataHolder getParentDataHolder() 315 { 316 return _modifiableParent; 317 } 318 319 @Override 320 public ModifiableIndexableDataHolder getRootDataHolder() 321 { 322 return _modifiableRoot; 323 } 324}