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}