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}