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.Optional; 023import java.util.Set; 024import java.util.TreeSet; 025 026import org.ametys.cms.data.holder.ModifiableIndexableDataHolder; 027import org.ametys.cms.data.holder.group.ModifiableIndexableRepeater; 028import org.ametys.cms.data.holder.group.ModifiableIndexableRepeaterEntry; 029import org.ametys.plugins.repository.RepositoryConstants; 030import org.ametys.plugins.repository.data.UnknownDataException; 031import org.ametys.plugins.repository.data.holder.DataHolder; 032import org.ametys.plugins.repository.data.holder.group.ModifiableModelAwareRepeaterEntry; 033import org.ametys.plugins.repository.data.holder.group.Repeater; 034import org.ametys.plugins.repository.data.holder.values.SynchronizableRepeater; 035import org.ametys.plugins.repository.data.holder.values.SynchronizationContext; 036import org.ametys.plugins.repository.data.holder.values.SynchronizationResult; 037import org.ametys.plugins.repository.data.repositorydata.ModifiableRepositoryData; 038import org.ametys.plugins.repository.lock.LockableAmetysObject; 039import org.ametys.plugins.repository.model.RepeaterDefinition; 040import org.ametys.runtime.model.ViewItemAccessor; 041import org.ametys.runtime.model.exception.BadItemTypeException; 042import org.ametys.runtime.model.exception.UndefinedItemPathException; 043 044/** 045 * CLass for modifiable model aware repeaters 046 */ 047public class DefaultModifiableModelAwareRepeater extends DefaultModelAwareRepeater implements ModifiableIndexableRepeater 048{ 049 private static final String __ENTRIES_TEMPORARY_NAME_PREFIX = "temp_"; 050 051 /** Modifiable repository data to use to store entries in the repository */ 052 protected ModifiableRepositoryData _modifiableRepositoryData; 053 054 /** Ametys object that can be locked on data modification */ 055 protected Optional<LockableAmetysObject> _lockableAmetysObject; 056 057 /** Parent of the current {@link Repeater} */ 058 protected ModifiableIndexableDataHolder _modifiableParent; 059 060 /** Root {@link DataHolder} */ 061 protected ModifiableIndexableDataHolder _modifiableRoot; 062 063 /** 064 * Creates a modifiable model aware repeater 065 * @param repositoryData the repository data of the repeater 066 * @param definition the definition of the repeater 067 * @param lockableAmetysObject the ametys object that can be locked on data modification 068 * @param parent the parent of the created {@link Repeater} 069 * @param root the root {@link DataHolder} 070 */ 071 public DefaultModifiableModelAwareRepeater(ModifiableRepositoryData repositoryData, RepeaterDefinition definition, Optional<LockableAmetysObject> lockableAmetysObject, ModifiableIndexableDataHolder parent, ModifiableIndexableDataHolder root) 072 { 073 super(repositoryData, definition, parent, root); 074 _modifiableRepositoryData = repositoryData; 075 _lockableAmetysObject = lockableAmetysObject; 076 _modifiableParent = parent; 077 _modifiableRoot = root; 078 } 079 080 @SuppressWarnings("unchecked") 081 @Override 082 public List<? extends ModifiableIndexableRepeaterEntry> getEntries() 083 { 084 return (List< ? extends ModifiableIndexableRepeaterEntry>) super.getEntries(); 085 } 086 087 @Override 088 public ModifiableIndexableRepeaterEntry getEntry(int position) 089 { 090 if (1 <= position && position <= getSize()) 091 { 092 ModifiableRepositoryData entryRepositoryData = _modifiableRepositoryData.getRepositoryData(String.valueOf(position)); 093 return new DefaultModifiableModelAwareRepeaterEntry(entryRepositoryData, _definition, _lockableAmetysObject, this); 094 } 095 else if (-getSize() < position && position <= 0) 096 { 097 // Find the positive equivalent position and call the getEntry method with this position 098 return getEntry(getSize() + position); 099 } 100 else 101 { 102 return null; 103 } 104 } 105 106 public ModifiableIndexableRepeaterEntry addEntry() 107 { 108 return addEntry(getSize() + 1); 109 } 110 111 public ModifiableIndexableRepeaterEntry addEntry(int position) throws IllegalArgumentException 112 { 113 if (1 <= position && position <= getSize() + 1) 114 { 115 // rename all entries that will be after the one that will be inserted 116 for (int currentEntryPosition = getSize(); currentEntryPosition >= position; currentEntryPosition--) 117 { 118 ModifiableRepositoryData entryRepositoryData = _modifiableRepositoryData.getRepositoryData(String.valueOf(currentEntryPosition)); 119 120 _lockableAmetysObject.ifPresent(lao -> lao.setLockInfoOnCurrentContext()); 121 entryRepositoryData.rename(String.valueOf(currentEntryPosition + 1)); 122 } 123 124 // Add the entry at the right position 125 _lockableAmetysObject.ifPresent(lao -> lao.setLockInfoOnCurrentContext()); 126 ModifiableRepositoryData createdRepositoryData = _modifiableRepositoryData.addRepositoryData(String.valueOf(position), RepositoryConstants.COMPOSITE_NODETYPE); 127 return new DefaultModifiableModelAwareRepeaterEntry(createdRepositoryData, _definition, _lockableAmetysObject, this); 128 } 129 else if (-getSize() <= position && position <= 0) 130 { 131 // Find the positive equivalent position and call the addEntry method with this position 132 return addEntry(getSize() + position + 1); 133 } 134 else 135 { 136 throw new IllegalArgumentException("The repeater named '" + _modifiableRepositoryData.getName() + "' has '" + getSize() + "' entries. You can not create an entry at position '" + position + "'."); 137 } 138 } 139 140 @SuppressWarnings("unchecked") 141 public <T extends SynchronizationResult> T synchronizeValues(ViewItemAccessor viewItemAccessor, SynchronizableRepeater repeaterValues, SynchronizationContext context) throws UndefinedItemPathException, BadItemTypeException 142 { 143 SynchronizationResult result = new SynchronizationResult(); 144 145 if (SynchronizableRepeater.Mode.APPEND.equals(repeaterValues.getMode())) 146 { 147 if (!repeaterValues.getRemovedEntries().isEmpty()) 148 { 149 removeEntries(repeaterValues.getRemovedEntries()); 150 result.setHasChanged(true); 151 } 152 153 for (Map<String, Object> entryValues : repeaterValues.getEntries()) 154 { 155 ModifiableModelAwareRepeaterEntry entry = addEntry(); 156 SynchronizationResult entryResult = entry.synchronizeValues(viewItemAccessor, entryValues, context); 157 result.aggregateResult(entryResult); 158 } 159 } 160 else if (SynchronizableRepeater.Mode.REPLACE.equals(repeaterValues.getMode())) 161 { 162 List<Integer> positions = repeaterValues.getReplacePositions(); 163 List<Map<String, Object>> entriesValues = repeaterValues.getEntries(); 164 165 for (int i = 0; i < positions.size(); i++) 166 { 167 int position = positions.get(i); 168 ModifiableModelAwareRepeaterEntry repeaterEntry = getEntry(position); 169 SynchronizationResult entryResult = repeaterEntry.synchronizeValues(viewItemAccessor, entriesValues.get(i), context); 170 result.aggregateResult(entryResult); 171 } 172 } 173 else // REPLACE_ALL mode 174 { 175 List<Map<String, Object>> entriesValues = repeaterValues.getEntries(); 176 boolean moved = moveEntries(repeaterValues.getPositionsMapping(), entriesValues.size()); 177 result.setHasChanged(moved); 178 179 for (ModifiableModelAwareRepeaterEntry repeaterEntry : getEntries()) 180 { 181 int entryIndex = repeaterEntry.getPosition() - 1; 182 Map<String, Object> entryValues = entriesValues.get(entryIndex); 183 SynchronizationResult entryResult = repeaterEntry.synchronizeValues(viewItemAccessor, entryValues, context); 184 result.aggregateResult(entryResult); 185 } 186 } 187 188 return (T) result; 189 } 190 191 public boolean moveEntries(Map<Integer, Integer> positionsMapping, int targetSize) 192 { 193 // For each entry, remove it if it does not appear in the mapping, or rename it with a temporary name 194 Map<Integer, String> temporaryNamesIndexedByPreviousPosition = new HashMap<>(); 195 int initialSize = getSize(); 196 boolean hasChanged = false; 197 for (int position = 1; position <= initialSize; position++) 198 { 199 if (positionsMapping.containsKey(position)) 200 { 201 if (!positionsMapping.get(position).equals(position)) 202 { 203 // Give a temporary name to the entry 204 String entryNewName = __ENTRIES_TEMPORARY_NAME_PREFIX + position; 205 ModifiableRepositoryData entryData = _modifiableRepositoryData.getRepositoryData(String.valueOf(position)); 206 entryData.rename(entryNewName); 207 temporaryNamesIndexedByPreviousPosition.put(position, entryNewName); 208 hasChanged = true; 209 } 210 } 211 else 212 { 213 // If the entry does not appear in the mapping, remove it 214 _lockableAmetysObject.ifPresent(lao -> lao.setLockInfoOnCurrentContext()); 215 _modifiableRepositoryData.removeValue(String.valueOf(position)); 216 hasChanged = true; 217 } 218 } 219 220 // Rename all existent entries with the new position 221 for (Integer previousPosition : temporaryNamesIndexedByPreviousPosition.keySet()) 222 { 223 String temporaryName = temporaryNamesIndexedByPreviousPosition.get(previousPosition); 224 ModifiableRepositoryData entryData = _modifiableRepositoryData.getRepositoryData(temporaryName); 225 226 Integer newPosition = positionsMapping.get(previousPosition); 227 entryData.rename(String.valueOf(newPosition)); 228 } 229 230 // Add the new entries to fill holes until the target size is reached 231 for (int position = 1; position <= targetSize; position++) 232 { 233 if (!_modifiableRepositoryData.hasValue(String.valueOf(position))) 234 { 235 _lockableAmetysObject.ifPresent(lao -> lao.setLockInfoOnCurrentContext()); 236 _modifiableRepositoryData.addRepositoryData(String.valueOf(position), RepositoryConstants.COMPOSITE_NODETYPE); 237 hasChanged = true; 238 } 239 } 240 241 return hasChanged; 242 } 243 244 public void removeEntries(Set<Integer> positions) throws UnknownDataException 245 { 246 // Create a list to sort the entries positions and remove from the last one to the first one 247 Set<Integer> sortedPositions = new TreeSet<>(Collections.reverseOrder()); 248 for (Integer position : positions) 249 { 250 if (1 <= position && position <= getSize()) 251 { 252 sortedPositions.add(position); 253 } 254 else if (-getSize() < position && position <= 0) 255 { 256 // Find the positive equivalent position 257 Integer equivalentPosition = getSize() + position; 258 sortedPositions.add(equivalentPosition); 259 } 260 else 261 { 262 throw new UnknownDataException("Unable to remove the entry at position '" + position + "' because there is no entry at this position."); 263 } 264 } 265 266 sortedPositions.stream() 267 .forEach(position -> this.removeEntry(position)); 268 } 269 270 public void removeEntry(int position) throws UnknownDataException 271 { 272 if (1 <= position && position <= getSize()) 273 { 274 // remove the entry 275 _lockableAmetysObject.ifPresent(lao -> lao.setLockInfoOnCurrentContext()); 276 _modifiableRepositoryData.removeValue(String.valueOf(position)); 277 278 // rename all entries after the removed one 279 for (int currentEntryPosition = position + 1; currentEntryPosition <= getSize() + 1; currentEntryPosition++) 280 { 281 ModifiableRepositoryData entryRepositoryData = _modifiableRepositoryData.getRepositoryData(String.valueOf(currentEntryPosition)); 282 entryRepositoryData.rename(String.valueOf(currentEntryPosition - 1)); 283 } 284 } 285 else if (-getSize() < position && position <= 0) 286 { 287 // Find the positive equivalent position and call the removeEntry method with this position 288 removeEntry(getSize() + position); 289 } 290 else 291 { 292 throw new UnknownDataException("Unable to remove the entry at position '" + position + "' because there is no entry at this position."); 293 } 294 } 295 296 @Override 297 public ModifiableRepositoryData getRepositoryData() 298 { 299 return _modifiableRepositoryData; 300 } 301 302 @Override 303 public ModifiableIndexableDataHolder getParentDataHolder() 304 { 305 return _modifiableParent; 306 } 307 308 @Override 309 public ModifiableIndexableDataHolder getRootDataHolder() 310 { 311 return _modifiableRoot; 312 } 313}