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.runtime.model;
017
018import java.util.ArrayDeque;
019import java.util.ArrayList;
020import java.util.Arrays;
021import java.util.Collection;
022import java.util.Deque;
023import java.util.HashMap;
024import java.util.HashSet;
025import java.util.LinkedHashMap;
026import java.util.List;
027import java.util.Map;
028import java.util.Optional;
029import java.util.Set;
030import java.util.UUID;
031import java.util.function.BiFunction;
032import java.util.function.Function;
033import java.util.stream.Collectors;
034
035import org.apache.commons.lang3.StringUtils;
036
037import org.ametys.runtime.model.exception.BadItemTypeException;
038import org.ametys.runtime.model.exception.UndefinedItemPathException;
039
040/**
041 * Helper class for views
042 */
043public final class ViewHelper
044{
045    private ViewHelper()
046    {
047        // Empty constructor
048    }
049    
050    /**
051     * The mode used to insert a view item in an existing view item accessor
052     */
053    public static enum InsertMode
054    {
055        /** Insert the view item before the referenced one */
056        BEFORE (0),
057        /** Insert the view item after the referenced one */
058        AFTER (1);
059
060        private final int _positionFromReferencedViewItem;
061
062        InsertMode(int offsetFromViewItem)
063        {
064            this._positionFromReferencedViewItem = offsetFromViewItem;
065        }
066
067        /**
068         * Retrieves the position of the item to insert compared to the referenced one
069         * @return the position of the item to insert compared to the referenced one
070         */
071        public int getPositionFromReferencedViewItem()
072        {
073            return _positionFromReferencedViewItem;
074        }
075        
076        @Override
077        public String toString()
078        {
079            return super.toString().toLowerCase();
080        }
081    }
082    
083    /**
084     * Creates an instance of {@link ModelViewItem} due to the given {@link ModelItem}
085     * @param modelItem the model item corresponding to the view item to create
086     * @return the created view item
087     */
088    public static ModelViewItem createModelViewItemInstance(ModelItem modelItem)
089    {
090        return modelItem instanceof ModelItemGroup
091                ? new ModelViewItemGroup()
092                : modelItem instanceof ModelItemAccessor
093                    ? new ViewElementAccessor()
094                    : new ViewElement();
095    }
096    
097    /**
098     * Retrieves the path of the given {@link ModelViewItem}. Only keep segments of the model view items, does not include groups
099     * @param modelViewItem the model view item
100     * @return the model view item's path
101     */
102    public static String getModelViewItemPath(ModelViewItem modelViewItem)
103    {
104        String path = modelViewItem.getName();
105        ViewItemAccessor parent = modelViewItem.getParent();
106        while (parent != null)
107        {
108            if (parent instanceof ModelViewItem parentModelViewItem)
109            {
110                path = parentModelViewItem.getName() + ModelItem.ITEM_PATH_SEPARATOR + path;
111            }
112            
113            parent = parent instanceof ViewItem parentViewItem ? parentViewItem.getParent() : null;
114        }
115        
116        return path;
117    }
118    
119    /**
120     * Copy the given view items. Also copy the children of view item accessors
121     * @param viewItems the view items to copy
122     * @return the view items copies
123     */
124    public static List<ViewItem> copyViewItems(List<ViewItem> viewItems)
125    {
126        return copyViewItems(viewItems, ViewHelper::createViewItemInstance);
127    }
128    
129    /**
130     * Copy the given view items. Also copy the children of view item accessors
131     * @param viewItems the view items to copy
132     * @param createCopyInstance the method to use to create the view item instances
133     * @return the view items copies
134     */
135    public static List<ViewItem> copyViewItems(List<ViewItem> viewItems, Function<ViewItem, ViewItem> createCopyInstance)
136    {
137        List<ViewItem> copies = new ArrayList<>();
138        
139        for (ViewItem viewItem : viewItems)
140        {
141            ViewItem copy = createCopyInstance.apply(viewItem);
142            viewItem.copyTo(copy);
143            
144            if (viewItem instanceof ViewItemAccessor viewItemAccessor)
145            {
146                List<ViewItem> copyChildren = copyViewItems(viewItemAccessor.getViewItems(), createCopyInstance);
147                ((ViewItemAccessor) copy).addViewItems(copyChildren);
148            }
149            
150            copies.add(copy);
151        }
152        
153        return copies;
154    }
155    
156    /**
157     * Create an instance of {@link ViewItem} from the given one
158     * @param viewItem the view item used to create the new instance of {@link ViewItem}
159     * @return the created {@link ViewItem} instance
160     */
161    public static ViewItem createViewItemInstance(ViewItem viewItem)
162    {
163        return viewItem.createInstance();
164    }
165    
166    /**
167     * Add the items of the view item accessor to include if they are not already present in the current view item accessor or in the referenced one
168     * @param currentAccessor the current accessor
169     * @param accessorToInclude the accessor to include
170     * @param referenceViewItemAccessor the reference view item accessor
171     * @param accessorPath the path of the accessor relative to the reference view item accessor
172     */
173    public static void addViewAccessorItems(ViewItemAccessor currentAccessor, ViewItemAccessor accessorToInclude, ViewItemAccessor referenceViewItemAccessor, String accessorPath)
174    {
175        for (ViewItem itemToInclude : accessorToInclude.getViewItems())
176        {
177            String itemPath = accessorPath;
178            if (itemToInclude instanceof ModelViewItem)
179            {
180                itemPath += StringUtils.isNotEmpty(accessorPath) ? ModelItem.ITEM_PATH_SEPARATOR + itemToInclude.getName() : itemToInclude.getName();
181            }
182
183            if (!(itemToInclude instanceof ModelViewItem) || !referenceViewItemAccessor.hasModelViewItem((ModelViewItem) itemToInclude, StringUtils.EMPTY, itemPath))
184            {
185                ViewItem copy = itemToInclude.createInstance();
186                currentAccessor.addViewItem(copy);
187                itemToInclude.copyTo(copy, referenceViewItemAccessor, itemPath);
188            }
189        }
190    }
191    
192    /**
193     * Creates a {@link ViewItemAccessor} with the items of the given {@link ModelItemAccessor}
194     * @param <T> Type of the {@link ViewItemAccessor} to create ({@link View}, {@link ModelViewItemGroup} or {@link ViewElementAccessor})
195     * @param modelItemAccessors the model item accessors
196     * @return the created {@link ViewItemAccessor}
197     * @throws IllegalArgumentException if the model item accessors collection is empty
198     */
199    public static <T extends ViewItemAccessor> T createViewItemAccessor(Collection<? extends ModelItemAccessor> modelItemAccessors) throws IllegalArgumentException
200    {
201        T viewItemAccessor = createEmptyViewItemAccessor(modelItemAccessors);
202        
203        for (ModelItemAccessor modelItemAccessor : modelItemAccessors)
204        {
205            for (ModelItem modelItem : modelItemAccessor.getModelItems())
206            {
207                if (!viewItemAccessor.hasModelViewItem(modelItem.getName()))
208                {
209                    addViewItem(modelItem.getName(), viewItemAccessor, modelItemAccessor);
210                }
211            }
212        }
213        
214        return viewItemAccessor;
215    }
216    
217    /**
218     * Creates a {@link ViewItemAccessor} with the given items
219     * @param <T> Type of the {@link ViewItemAccessor} to create ({@link View}, {@link ModelViewItemGroup} or {@link ViewElementAccessor})
220     * @param modelItemAccessors the model items accessing the items definitions
221     * @param itemPaths the paths of the items to put in the view item accessor
222     * @return the created {@link ViewItemAccessor}
223     * @throws IllegalArgumentException if the model item containers collection is empty or if an item path is <code>null</code>, empty, or is not defined in the given model item containers
224     * @throws BadItemTypeException if a segment in a path (but not the last) does not represent a group item
225     */
226    public static <T extends ViewItemAccessor> T createViewItemAccessor(Collection<? extends ModelItemAccessor> modelItemAccessors, String... itemPaths) throws IllegalArgumentException, BadItemTypeException
227    {
228        T viewItemAccessor = createEmptyViewItemAccessor(modelItemAccessors);
229        
230        for (String itemPath : itemPaths)
231        {
232            if (ModelHelper.hasModelItem(itemPath, modelItemAccessors))
233            {
234                addViewItem(itemPath, viewItemAccessor, modelItemAccessors.toArray(new ModelItemAccessor[modelItemAccessors.size()]));
235            }
236            else
237            {
238                String modelIds = modelItemAccessors.stream()
239                                                    .map(modelItemContainer -> _getModelItemAccessorIdentifier(modelItemContainer))
240                                                    .flatMap(Optional::stream)
241                                                    .collect(Collectors.joining(", "));
242                throw new IllegalArgumentException("Item '" + itemPath + "' not found in models: '" + modelIds + "'.");
243            }
244        }
245        
246        return viewItemAccessor;
247    }
248    
249    /**
250     * Creates an empty {@link ViewItemAccessor}
251     * The created container can be a {@link View}, a {@link ModelViewItemGroup} or a {@link ViewElementAccessor}, according to the given {@link ModelItemAccessor}s.
252     * @param <T> The type of the created accessor
253     * @param modelItemAccessors the model items accessing the items definitions
254     * @return the created {@link ViewItemAccessor}
255     * @throws IllegalArgumentException if the model item accessors collection is empty
256     */
257    @SuppressWarnings("unchecked")
258    public static <T extends ViewItemAccessor> T createEmptyViewItemAccessor(Collection<? extends ModelItemAccessor> modelItemAccessors) throws IllegalArgumentException
259    {
260        if (modelItemAccessors.isEmpty())
261        {
262            throw new IllegalArgumentException("The model is needed to create a view item container");
263        }
264        else
265        {
266            ModelItemAccessor firstModelItemAccessor = modelItemAccessors.iterator().next();
267            if (firstModelItemAccessor instanceof ElementDefinition)
268            {
269                ViewElementAccessor viewItemAccesor = new ViewElementAccessor();
270                viewItemAccesor.setDefinition((ElementDefinition) firstModelItemAccessor);
271                return (T) viewItemAccesor;
272            }
273            else if (firstModelItemAccessor instanceof ModelItemGroup)
274            {
275                ModelViewItemGroup viewItemAccessor = new ModelViewItemGroup();
276                viewItemAccessor.setDefinition((ModelItemGroup) firstModelItemAccessor);
277                return (T) viewItemAccessor;
278            }
279            else
280            {
281                return (T) new View();
282            }
283        }
284    }
285    
286    /**
287     * Add a view item in the given accessor.
288     * Only one view item is created for a given container. All paths with the same container are added in the same view item.
289     * If the last segment of the given path is a {@link ModelItemContainer}, all its children are added to the created view item
290     * @param relativePath path of the item to add
291     * @param viewItemAccessor the view item accessor
292     * @param modelItemAccessors the corresponding model item accessors
293     * @return the added view item
294     * @throws IllegalArgumentException if the path is <code>null</code> or empty
295     * @throws UndefinedItemPathException if the path is not defined in the given model item accessors
296     * @throws BadItemTypeException if a segment in a path (but not the last) does not represent a group item
297     */
298    public static ModelViewItem addViewItem(String relativePath, ViewItemAccessor viewItemAccessor, ModelItemAccessor... modelItemAccessors) throws IllegalArgumentException, UndefinedItemPathException, BadItemTypeException
299    {
300        return addViewItem(relativePath, viewItemAccessor, true, true, modelItemAccessors);
301    }
302    
303    /**
304     * Add a view item in the given accessor.
305     * If mergeContainers is set to <code>true</code>, only one view item is created for a given container. All paths with the same container are added in the same view item.
306     * If the last segment of the given path is a {@link ModelItemContainer} and expandContainers is set to <code>true</code>, all its children are added to the created view item
307     * @param relativePath path of the item to add
308     * @param viewItemAccessor the view item accessor
309     * @param expandContainers set to <code>false</code> to avoid to create view items for all children of the created view item if it is a container
310     * @param mergeContainers set to <code>true</code> to avoid to create new containers when they are already present in the given view item accessor
311     * @param modelItemAccessors the corresponding model item accessors
312     * @return the added view item
313     * @throws IllegalArgumentException if the path is <code>null</code> or empty
314     * @throws UndefinedItemPathException if the path is not defined in the given model item accessors
315     * @throws BadItemTypeException if a segment in a path (but not the last) does not represent a group item
316     */
317    @SuppressWarnings("unchecked")
318    public static ModelViewItem addViewItem(String relativePath, ViewItemAccessor viewItemAccessor, boolean expandContainers, boolean mergeContainers, ModelItemAccessor... modelItemAccessors) throws IllegalArgumentException, UndefinedItemPathException, BadItemTypeException
319    {
320        int firstIndexOfItemPathSeparator = relativePath.indexOf(ModelItem.ITEM_PATH_SEPARATOR);
321        String firstPathSegment = firstIndexOfItemPathSeparator > -1 ? relativePath.substring(0, relativePath.indexOf(ModelItem.ITEM_PATH_SEPARATOR)) : relativePath;
322        
323        ModelItem modelItem = ModelHelper.getModelItem(firstPathSegment, Arrays.asList(modelItemAccessors));
324        
325        // Create the view item and add it to the current view item container
326        ModelViewItem viewItem = null;
327        if (modelItem instanceof ModelItemAccessor)
328        {
329            if (mergeContainers && viewItemAccessor.hasModelViewItem(firstPathSegment))
330            {
331                viewItem = viewItemAccessor.getModelViewItem(firstPathSegment);
332            }
333            else
334            {
335                if (modelItem instanceof ModelItemGroup)
336                {
337                    viewItem = new ModelViewItemGroup();
338                }
339                else
340                {
341                    viewItem = new ViewElementAccessor();
342                }
343                viewItem.setDefinition(modelItem);
344                viewItemAccessor.addViewItem(viewItem);
345            }
346        }
347        else
348        {
349            viewItem = new ViewElement();
350            viewItem.setDefinition(modelItem);
351            viewItemAccessor.addViewItem(viewItem);
352        }
353
354        if (firstIndexOfItemPathSeparator > -1)
355        {
356            if (modelItem instanceof ModelItemAccessor)
357            {
358                // Only the first segment of the path has been processed, now recursively process the next ones
359                String subPath = relativePath.substring(firstIndexOfItemPathSeparator + 1);
360                return addViewItem(subPath, (ViewItemAccessor) viewItem, expandContainers, mergeContainers, (ModelItemAccessor) modelItem);
361            }
362            else
363            {
364                // The path has several segments but the first one is not a view item accessor
365                throw new BadItemTypeException("The segments inside the items' paths can only refer to an accessor. The path '" + relativePath + "' refers to the item '" + modelItem.getPath() + "' that is not an accessor");
366            }
367        }
368        else if (modelItem instanceof ModelItemContainer && expandContainers)
369        {
370            // The last segment is a container, add all its children in the current view item container
371            for (ModelItem child : ((ModelItemContainer) modelItem).getModelItems())
372            {
373                String newRelativePath = StringUtils.removeStart(child.getPath(), modelItem.getPath() + ModelItem.ITEM_PATH_SEPARATOR);
374                addViewItem(newRelativePath, (ViewItemAccessor) viewItem, expandContainers, mergeContainers, (ModelItemContainer) modelItem);
375            }
376        }
377        
378        return viewItem;
379    }
380
381    private static Optional<String> _getModelItemAccessorIdentifier(ModelItemAccessor accessor)
382    {
383        if (accessor instanceof Model)
384        {
385            return Optional.ofNullable(((Model) accessor).getId());
386        }
387        else if (accessor instanceof ModelItem)
388        {
389            return Optional.ofNullable(((ModelItem) accessor).getName());
390        }
391        else
392        {
393            return Optional.empty();
394        }
395    }
396    
397    /**
398     * Retrieves the {@link SimpleViewItemGroup} at the given group path. All segments in the group path must refer to a {@link SimpleViewItemGroup} (tab, fieldset)
399     * If there are more than one corresponding items, the first one is retrieved
400     * @param viewItemAccessor The ViewItemAccessor in which to find the group of given path
401     * @param groupPath The group path
402     * @throws IllegalArgumentException if the group path is empty
403     * @throws UndefinedItemPathException if no group was found at the given path
404     * @return the found {@link SimpleViewItemGroup}
405     */
406    public static SimpleViewItemGroup getSimpleViewItemGroup(ViewItemAccessor viewItemAccessor, String groupPath)
407    {
408        String[] pathSegments = groupPath.split(ModelItem.ITEM_PATH_SEPARATOR);
409        
410        if (pathSegments == null || pathSegments.length < 1)
411        {
412            throw new IllegalArgumentException("Unable to retrieve the group at the given path. This path is empty.");
413        }
414        else if (pathSegments.length == 1)
415        {
416            String groupName = groupPath;
417            return Optional.ofNullable(viewItemAccessor.getViewItem(groupName))
418                                   .filter(SimpleViewItemGroup.class::isInstance)
419                                   .map(SimpleViewItemGroup.class::cast)
420                                   .orElseThrow(() -> new UndefinedItemPathException("Unable to retrieve the group with the name '" + groupName + "'. No group with this name has been found in view item accessor '" + _getViewItemAccessorName(viewItemAccessor) + "'"));
421        }
422        else
423        {
424            SimpleViewItemGroup simpleViewItemGroup = getSimpleViewItemGroup(viewItemAccessor, pathSegments[0]);
425            String subGroupPath = StringUtils.join(pathSegments, ModelItem.ITEM_PATH_SEPARATOR, 1, pathSegments.length);
426            return getSimpleViewItemGroup(simpleViewItemGroup, subGroupPath);
427        }
428    }
429    
430    /**
431     * Retrieves the {@link ViewItem} at the given view path. If items are in groups, the names of the groups must appear in the path. All groups have to be named
432     * If there are more than one corresponding items, the first one is retrieved
433     * @param viewItemAccessor The ViewItemAccessor in which to find the view item of given path
434     * @param viewItemPath The view item path
435     * @throws IllegalArgumentException if the view item path is empty
436     * @throws UndefinedItemPathException if no view item was found at the given path
437     * @return the found {@link ViewItem}
438     */
439    public static ViewItem getViewItem(ViewItemAccessor viewItemAccessor, String viewItemPath)
440    {
441        String[] pathSegments = viewItemPath.split(ModelItem.ITEM_PATH_SEPARATOR);
442        
443        if (pathSegments == null || pathSegments.length < 1)
444        {
445            throw new IllegalArgumentException("Unable to retrieve the view item at the given path. This path is empty.");
446        }
447        else if (pathSegments.length == 1)
448        {
449            String viewItemName = viewItemPath;
450            return Optional.ofNullable(viewItemAccessor.getViewItem(viewItemName))
451                                   .orElseThrow(() -> new UndefinedItemPathException("Unable to retrieve the view item with the name '" + viewItemName + "'. No view item with this name has been found in view item accessor '" + _getViewItemAccessorName(viewItemAccessor) + "'"));
452        }
453        else
454        {
455            ViewItemAccessor newViewItemAccessor = viewItemAccessor.getViewItems().stream()
456                                                                   .filter(viewItem -> pathSegments[0].equals(viewItem.getName()))
457                                                                   .filter(ViewItemAccessor.class::isInstance)
458                                                                   .map(ViewItemAccessor.class::cast)
459                                                                   .findFirst()
460                                                                   .orElseThrow(() -> new UndefinedItemPathException("Unable to retrieve the view item accessor with the name '" + pathSegments[0] + "'. No view item accessor with this name has been found in view item accessor '" + _getViewItemAccessorName(viewItemAccessor) + "'"));
461            String subviewItemPath = StringUtils.join(pathSegments, ModelItem.ITEM_PATH_SEPARATOR, 1, pathSegments.length);
462            return getViewItem(newViewItemAccessor, subviewItemPath);
463        }
464    }
465    
466    /**
467     * Insert the given {@link ViewItem} to the view after the item at the given path
468     * @param viewItemAccessor The ViewItemAccessor in which to insert the view item
469     * @param viewItemToInsert The view item to insert
470     * @param insertAfter The name of the view item after which the given one has to be inserted. The item with this name must appear in the given accessor
471     * @throws IllegalArgumentException If the given view item path is null or empty
472     * @throws UndefinedItemPathException If the given view item path is not present in the view item accessor
473     */
474    public static void insertViewItemAfter(ViewItemAccessor viewItemAccessor, ViewItem viewItemToInsert, String insertAfter)  throws IllegalArgumentException, UndefinedItemPathException
475    {
476        insertItemAfterOrBefore(viewItemAccessor, viewItemToInsert, insertAfter, InsertMode.AFTER);
477    }
478    
479    /**
480     * Insert the given {@link ViewItem} to the view before the item at the given path
481     * @param viewItemAccessor The ViewItemAccessor in which to insert the item
482     * @param viewItemToInsert The view item to insert
483     * @param insertBefore The name of the view item before which the given one has to be inserted. The item with this name must appear in the given accessor
484     * @throws IllegalArgumentException If the ModelItem "insertBefore" is null or empty
485     * @throws UndefinedItemPathException If the path to the modelItem "insertBefore" is undefined
486     */
487    public static void insertItemBefore(ViewItemAccessor viewItemAccessor, ViewItem viewItemToInsert, String insertBefore)  throws IllegalArgumentException, UndefinedItemPathException
488    {
489        insertItemAfterOrBefore(viewItemAccessor, viewItemToInsert, insertBefore, InsertMode.BEFORE);
490    }
491
492    /**
493     * Insert the given {@link ViewItem} to the view before or after the item at the given path
494     * @param viewItemAccessor The ViewItemAccessor in which to insert the item
495     * @param itemToInsert The view item to insert
496     * @param insertAfterOrBefore The name of the view item before or after which the given one has to be inserted. The item with this name must appear in the given accessor
497     * @param insertMode The mode of insertion (before or after)
498     * @throws IllegalArgumentException If the ModelItem "insertAfterOrBefore" is null, empty, or is a path
499     * @throws UndefinedItemPathException If the path to the modelItem "insertAfterOrBefore" is undefined
500     */
501    public static void insertItemAfterOrBefore(ViewItemAccessor viewItemAccessor, ViewItem itemToInsert, String insertAfterOrBefore, InsertMode insertMode) throws IllegalArgumentException, UndefinedItemPathException
502    {
503        // Checks that the given item path is not empty
504        if (StringUtils.isBlank(insertAfterOrBefore))
505        {
506            throw new IllegalArgumentException("Unable to insert view item " + itemToInsert.getName() + " " + insertMode.toString() + " the view item with the given name. This name is empty.");
507        }
508        
509        // Checks that the given item path is not a path
510        if (insertAfterOrBefore.contains(ModelItem.ITEM_PATH_SEPARATOR))
511        {
512            throw new IllegalArgumentException("Unable to insert view item " + itemToInsert.getName() + " " + insertMode.toString() + " the view item with the name '" + insertAfterOrBefore + "'. This name is a path.");
513        }
514        
515        if (!_doInsertItemAfterOrBefore(viewItemAccessor, itemToInsert, insertAfterOrBefore, insertMode))
516        {
517            throw new UndefinedItemPathException("Unable to insert view item " + itemToInsert.getName() + " " + insertMode.toString() + " the view item at path " + insertAfterOrBefore + ". No view item has been found at this path in view item accessor '" + _getViewItemAccessorName(viewItemAccessor) + "'");
518        }
519    }
520    
521    private static boolean _doInsertItemAfterOrBefore(ViewItemAccessor viewItemAccessor, ViewItem itemToInsert, String insertAfterOrBefore, InsertMode insertMode)
522    {
523        List<ViewItem> viewItems = viewItemAccessor.getViewItems();
524        for (int indexInAccessor = 0; indexInAccessor < viewItems.size(); indexInAccessor++)
525        {
526            ViewItem viewItem = viewItems.get(indexInAccessor);
527            if (insertAfterOrBefore.equals(viewItem.getName()))
528            {
529                // If the viewItemAccessor is a ModelViewItem, then check if the itemToInsert is a part of the viewItemAccessor's model
530                if (viewItemAccessor instanceof ModelViewItem modelViewItem && !((ModelItemAccessor) modelViewItem.getDefinition()).hasModelItem(itemToInsert.getName()))
531                {
532                    throw new UndefinedItemPathException("Unable to insert view item " + itemToInsert.getName() + " " + insertMode.toString() + " the view item at path " + insertAfterOrBefore + ". The item to insert is not defined by the model.");
533                }
534                
535                // Insert the view item to insert, in the view
536                viewItemAccessor.insertViewItem(itemToInsert, indexInAccessor + insertMode.getPositionFromReferencedViewItem());
537                return true;
538            }
539        }
540        
541        return false;
542    }
543    
544    private static String _getViewItemAccessorName(ViewItemAccessor viewItemAccessor)
545    {
546        return viewItemAccessor instanceof View view
547                ? view.getName()
548                : viewItemAccessor instanceof ViewItem viewItem
549                    ? viewItem.getName()
550                    : viewItemAccessor.toString();
551    }
552    
553    /**
554     * Retrieves the paths of all model items in the given {@link View}
555     * @param view the {@link View}
556     * @return the paths of all items
557     */
558    public static Set<String> getModelItemsPathsFromView(View view)
559    {
560        return _getModelItemsFromViewItemContainer(view).keySet();
561    }
562    
563    /**
564     * Retrieves all model items in the given {@link View}
565     * @param view the {@link View}
566     * @return all model items
567     */
568    public static Collection<ModelItem> getModelItemsFromView(View view)
569    {
570        return _getModelItemsFromViewItemContainer(view).values();
571    }
572    
573    /**
574     * Retrieves all model items in the given {@link ViewItemContainer}, indexed by their paths
575     * @param viewItemContainer the {@link ViewItemContainer}
576     * @return all model items
577     */
578    private static Map<String, ModelItem> _getModelItemsFromViewItemContainer(ViewItemContainer viewItemContainer)
579    {
580        Map<String, ModelItem> result = new HashMap<>();
581        
582        for (ViewItem viewItem : viewItemContainer.getViewItems())
583        {
584            if (viewItem instanceof ModelViewItem)
585            {
586                ModelItem modelItem = ((ModelViewItem) viewItem).getDefinition();
587                result.put(modelItem.getPath(), modelItem);
588            }
589            
590            if (viewItem instanceof ViewItemContainer)
591            {
592                result.putAll(_getModelItemsFromViewItemContainer((ViewItemContainer) viewItem));
593            }
594        }
595        
596        return result;
597    }
598    
599    /**
600     * Checks if all the items of the given view are present only once in the view
601     * Children of accessors that are not containers are not taken into account
602     * @param view the view to check
603     * @return <code>true</code> if all items are presents only once, <code>false</code> otherwise
604     */
605    public static boolean areItemsPresentsOnlyOnce(View view)
606    {
607        return _areItemsPresentsOnlyOnce(view, new HashSet<>());
608    }
609    
610    private static boolean _areItemsPresentsOnlyOnce(ViewItemContainer containerToCheck, Set<String> paths)
611    {
612        for (ViewItem viewItem : containerToCheck.getViewItems())
613        {
614            if (viewItem instanceof ModelViewItem)
615            {
616                String viewItemPath = ((ModelViewItem) viewItem).getDefinition().getPath();
617                
618                if (paths.contains(viewItemPath))
619                {
620                    return false;
621                }
622                else
623                {
624                    paths.add(viewItemPath);
625                }
626            }
627
628            if (viewItem instanceof ViewItemContainer && !_areItemsPresentsOnlyOnce((ViewItemContainer) viewItem, paths))
629            {
630                return false;
631            }
632        }
633        
634        return true;
635    }
636    
637    /**
638     * Retrieves a View corresponding to the given one, avoiding the view items below the {@link ViewItemAccessor}s that are not {@link ViewItemContainer}s
639     * @param originalView the view to truncate
640     * @return the truncated view
641     */
642    public static View getTruncatedView(View originalView)
643    {
644        View view = new View();
645        view.addViewItems(_copyItemsForTruncatedView(originalView));
646        return view;
647    }
648    
649    private static List<ViewItem> _copyItemsForTruncatedView(ViewItemContainer currentOrigContainer)
650    {
651        List<ViewItem> copies = new ArrayList<>();
652        for (ViewItem origChild : currentOrigContainer.getViewItems())
653        {
654            // do not copy non editable item for the truncated view
655            if (_isEditable(origChild))
656            {
657                ViewItem destChild = origChild.createInstance();
658                origChild.copyTo(destChild);
659                
660                // Get children of containers, not accessors for the truncated view
661                if (origChild instanceof ViewItemContainer)
662                {
663                    ((ViewItemContainer) destChild).addViewItems(_copyItemsForTruncatedView((ViewItemContainer) origChild));
664                }
665                
666                copies.add(destChild);
667            }
668        }
669        
670        return copies;
671    }
672    
673    private static boolean _isEditable(ViewItem viewItem)
674    {
675        return viewItem instanceof ViewElement viewElement ? viewElement.getDefinition().isEditable() : true;
676    }
677    
678    /**
679     * Retrieves a {@link ViewItemAccessor} corresponding to the given one, where items that appears several times are merged
680     * @param <T> The type of {@link ViewItemAccessor} to merge
681     * @param originalViewItemAccessor the {@link ViewItemAccessor} to merge
682     * @return the merged {@link ViewItemAccessor}
683     */
684    @SuppressWarnings("unchecked")
685    public static <T extends ViewItemAccessor> T mergeDuplicatedItems(T originalViewItemAccessor)
686    {
687        ViewItemAccessor destinationViewItemAccessor;
688        if (originalViewItemAccessor instanceof View originalView)
689        {
690            View destinationView = new View();
691            originalView.copyTo(destinationView);
692            destinationViewItemAccessor = destinationView;
693        }
694        else
695        {
696            ViewItem destinationViewItem = ((ViewItem) originalViewItemAccessor).createInstance();
697            ((ViewItem) originalViewItemAccessor).copyTo(destinationViewItem);
698            destinationViewItemAccessor = (ViewItemAccessor) destinationViewItem;
699        }
700        
701        _mergeDuplicatedItems(originalViewItemAccessor, destinationViewItemAccessor, destinationViewItemAccessor, StringUtils.EMPTY);
702        
703        return (T) destinationViewItemAccessor;
704    }
705    
706    private static void _mergeDuplicatedItems(ViewItemAccessor currentOrigAccessor, ViewItemAccessor currentDestAccessor, ViewItemAccessor destinationViewItemAccessor, String accessorPath)
707    {
708        for (ViewItem origChild : currentOrigAccessor.getViewItems())
709        {
710            if (origChild instanceof ModelViewItem)
711            {
712                String itemPath = StringUtils.isNotEmpty(accessorPath) ? accessorPath + ModelItem.ITEM_PATH_SEPARATOR + origChild.getName() : origChild.getName();
713                if (ViewHelper.hasModelViewItem(destinationViewItemAccessor, itemPath))
714                {
715                    // Ignore View elements, check view item accessors
716                    if (origChild instanceof ViewItemAccessor)
717                    {
718                        ViewItem destChild = ViewHelper.getModelViewItem(destinationViewItemAccessor, itemPath);
719                        _mergeDuplicatedItems((ViewItemAccessor) origChild, (ViewItemAccessor) destChild, destinationViewItemAccessor, itemPath);
720                    }
721                }
722                else
723                {
724                    ViewItem destChild = origChild.createInstance();
725                    currentDestAccessor.addViewItem(destChild);
726                    
727                    if (origChild instanceof ViewItemAccessor)
728                    {
729                        origChild.copyTo(destChild);
730                        _mergeDuplicatedItems((ViewItemAccessor) origChild, (ViewItemAccessor) destChild, destinationViewItemAccessor, itemPath);
731                    }
732                    else
733                    {
734                        origChild.copyTo(destChild, destinationViewItemAccessor, itemPath);
735                    }
736                }
737            }
738            else
739            {
740                ViewItem destChild = origChild.createInstance();
741                currentDestAccessor.addViewItem(destChild);
742                origChild.copyTo(destChild);
743                _mergeDuplicatedItems((ViewItemAccessor) origChild, (ViewItemAccessor) destChild, destinationViewItemAccessor, accessorPath);
744            }
745        }
746    }
747    
748    /**
749     * Checks if there is a {@link ModelViewItem} in the {@link ViewItemAccessor} at the given path
750     * @param viewItemAccessor The accessor of the view items
751     * @param itemPath The path of the item to check
752     * @return <code>true</code> if there is a model view item at the given path, <code>false</code> otherwise
753     */
754    public static boolean hasModelViewItem(ViewItemAccessor viewItemAccessor, String itemPath)
755    {
756        try
757        {
758            getModelViewItem(viewItemAccessor, itemPath);
759            // The model item can never be null. If no excpetion has been thrown, the there is a model item at this path
760            return true;
761        }
762        catch (UndefinedItemPathException | BadItemTypeException e)
763        {
764            return false;
765        }
766    }
767    
768    /**
769     * Converts the given view items as a JSON map
770     * @param viewItems the view items to convert
771     * @param context the context of the items' definitions
772     * @return The view items as a JSON map
773     */
774    public static Map<String, Object> viewItemsToJSON(List<ViewItem> viewItems, DefinitionContext context)
775    {
776        Map<String, Object> elements = new LinkedHashMap<>();
777        
778        for (ViewItem item : viewItems)
779        {
780            Map<String, Object> itemAsJSON = item.toJSON(context);
781
782            if (!itemAsJSON.isEmpty())
783            {
784                String itemUUID = item.getName();
785                if (StringUtils.isEmpty(itemUUID))
786                {
787                    itemUUID = UUID.randomUUID().toString();
788                }
789                
790                elements.put(itemUUID, itemAsJSON);
791            }
792        }
793        
794        return elements;
795    }
796    
797    /**
798     * Gets the {@link ModelViewItem} from the {@link ViewItemAccessor} at the given path.
799     * <br>Unlike {@link View#getModelViewItem(String)}, this method accepts a path and not only a name, allowing to traverse composites.
800     * @param viewItemAccessor The accessor of view items
801     * @param itemPath The path of the item to get
802     * @return The {@link ModelViewItem}. Can never be <code>null</code>
803     * @throws UndefinedItemPathException If one of the parts of the given path is undefined
804     * @throws BadItemTypeException If one of the parts of the given path is defined, but is not of the correct type
805     */
806    public static ModelViewItem getModelViewItem(ViewItemAccessor viewItemAccessor, String itemPath) throws UndefinedItemPathException, BadItemTypeException
807    {
808        return new ViewItemGetter(viewItemAccessor).getViewItem(itemPath);
809    }
810    
811    /**
812     * Gets the {@link ViewElement} from the {@link ViewItemAccessor} at the given path.
813     * <br>Unlike {@link View#getModelViewItem(String)}, this method accepts a path and not only a name, allowing to traverse composites.
814     * @param viewItemAccessor The accessor of view items
815     * @param itemPath The path of the item to get
816     * @return The {@link ViewElement}. Can never be <code>null</code>
817     * @throws UndefinedItemPathException If one of the parts of the given path is undefined
818     * @throws BadItemTypeException If one of the parts of the given path is defined, but is not of the correct type
819     */
820    public static ViewElement getViewElement(ViewItemAccessor viewItemAccessor, String itemPath) throws UndefinedItemPathException, BadItemTypeException
821    {
822        return new ViewItemGetter(viewItemAccessor).getModelViewItem(itemPath);
823    }
824    
825    /**
826     * Gets the {@link ModelViewItemGroup} from the {@link ViewItemAccessor} at the given path.
827     * <br>Unlike {@link View#getModelViewItem(String)}, this method accepts a path and not only a name, allowing to traverse composites.
828     * @param viewItemAccessor The accessor of view items
829     * @param itemPath The path of the container to get
830     * @return The {@link ModelViewItemGroup}. Can never be <code>null</code>
831     * @throws UndefinedItemPathException If one of the parts of the given path is undefined
832     * @throws BadItemTypeException If one of the parts of the given path is defined, but is not of the correct type
833     */
834    public static ModelViewItemGroup getModelViewItemGroup(ViewItemAccessor viewItemAccessor, String itemPath) throws UndefinedItemPathException, BadItemTypeException
835    {
836        return new ViewItemGetter(viewItemAccessor).getViewItemContainer(itemPath);
837    }
838    
839    private static class ViewItemGetter
840    {
841        private final ViewItemAccessor _viewItemAccessor;
842        private String _wholePath;
843        
844        ViewItemGetter(ViewItemAccessor viewItemAccessor)
845        {
846            _viewItemAccessor = viewItemAccessor;
847        }
848        
849        ModelViewItem getViewItem(String wholePath) throws UndefinedItemPathException, BadItemTypeException
850        {
851            _wholePath = wholePath;
852            BiFunction<ViewItemAccessor, String, ModelViewItem> lastPartGetter = (viewItemAccessor, lastPart) -> _getDirectViewItem(viewItemAccessor, lastPart, ModelViewItem.class);
853            return _getModelViewItem(lastPartGetter);
854        }
855        
856        ViewElement getModelViewItem(String wholePath) throws UndefinedItemPathException, BadItemTypeException
857        {
858            _wholePath = wholePath;
859            BiFunction<ViewItemAccessor, String, ViewElement> lastPartGetter = (viewItemAccessor, lastPart) -> _getDirectViewItem(viewItemAccessor, lastPart, ViewElement.class);
860            return _getModelViewItem(lastPartGetter);
861        }
862        
863        ModelViewItemGroup getViewItemContainer(String wholePath) throws UndefinedItemPathException, BadItemTypeException
864        {
865            _wholePath = wholePath;
866            BiFunction<ViewItemAccessor, String, ModelViewItemGroup> lastPartGetter = (viewItemAccessor, lastPart) -> _getDirectModelViewItemGroup(viewItemAccessor, lastPart);
867            return _getModelViewItem(lastPartGetter);
868        }
869        
870        private <T> T _getModelViewItem(BiFunction<ViewItemAccessor, String, T> lastPartGetter) throws UndefinedItemPathException, BadItemTypeException
871        {
872            Deque<String> parts = new ArrayDeque<>(Arrays.asList(_wholePath.split(ModelItem.ITEM_PATH_SEPARATOR)));
873            String lastPart = parts.removeLast();
874            ViewItemAccessor currentViewItemAccessor = _viewItemAccessor;
875            while (!parts.isEmpty())
876            {
877                String currentPart = parts.pop();
878                currentViewItemAccessor =  _getDirectViewItem(currentViewItemAccessor, currentPart, ViewItemAccessor.class);
879            }
880            
881            return lastPartGetter.apply(currentViewItemAccessor, lastPart);
882        }
883        
884        private ModelViewItemGroup _getDirectModelViewItemGroup(ViewItemAccessor viewItemAccessor, String itemName) throws UndefinedItemPathException, BadItemTypeException
885        {
886            return _getDirectViewItem(viewItemAccessor, itemName, ModelViewItemGroup.class);
887        }
888        
889        private <T> T _getDirectViewItem(ViewItemAccessor viewItemAccessor, String itemName, Class<T> resultClass) throws UndefinedItemPathException, BadItemTypeException
890        {
891            if (!viewItemAccessor.hasModelViewItem(itemName))
892            {
893                throw new UndefinedItemPathException("For path '" + _wholePath + "', the part '" + itemName + "' is not defined");
894            }
895            else
896            {
897                ModelViewItem modelViewItem = viewItemAccessor.getModelViewItem(itemName);
898                if (resultClass.isInstance(modelViewItem))
899                {
900                    return resultClass.cast(modelViewItem);
901                }
902                else
903                {
904                    throw new BadItemTypeException("For path '" + _wholePath + "', the part '" + itemName + "' does not point to a '" + resultClass + "' (got a '" + modelViewItem.getClass().getName() + "')");
905                }
906            }
907        }
908    }
909    
910    /**
911     * Get the collection of model items from a view item accessor.
912     * @param viewItemAccessor The view item accessor
913     * @return a collection of model items
914     */
915    public static Collection<ModelItem> getModelItems(ViewItemAccessor viewItemAccessor)
916    {
917        Set<ModelItem> modelItems = new HashSet<>();
918        
919        for (ViewItem viewItem : viewItemAccessor.getViewItems())
920        {
921            if (viewItem instanceof ViewItemGroup viewItemGroup)
922            {
923                modelItems.addAll(getModelItems(viewItemGroup));
924            }
925            else if (viewItem instanceof ViewElement viewElement)
926            {
927                modelItems.add(viewElement.getDefinition());
928            }
929        }
930        
931        return modelItems;
932    }
933}