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