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