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