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}