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 = viewItemAccessor.getViewItems().stream()
351                                                                   .filter(viewItem -> pathSegments[0].equals(viewItem.getName()))
352                                                                   .filter(ViewItemAccessor.class::isInstance)
353                                                                   .map(ViewItemAccessor.class::cast)
354                                                                   .findFirst()
355                                                                   .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) + "'"));
356            String subviewItemPath = StringUtils.join(pathSegments, ModelItem.ITEM_PATH_SEPARATOR, 1, pathSegments.length);
357            return getViewItem(newViewItemAccessor, subviewItemPath);
358        }
359    }
360    
361    /**
362     * Insert the given {@link ViewItem} to the view after the item at the given path
363     * @param viewItemAccessor The ViewItemAccessor in which to insert the view item
364     * @param viewItemToInsert The view item to insert
365     * @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
366     * @throws IllegalArgumentException If the given view item path is null or empty
367     * @throws UndefinedItemPathException If the given view item path is not present in the view item accessor
368     */
369    public static void insertViewItemAfter(ViewItemAccessor viewItemAccessor, ViewItem viewItemToInsert, String insertAfter)  throws IllegalArgumentException, UndefinedItemPathException
370    {
371        insertItemAfterOrBefore(viewItemAccessor, viewItemToInsert, insertAfter, InsertMode.AFTER);
372    }
373    
374    /**
375     * Insert the given {@link ViewItem} to the view before the item at the given path
376     * @param viewItemAccessor The ViewItemAccessor in which to insert the item
377     * @param viewItemToInsert The view item to insert
378     * @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
379     * @throws IllegalArgumentException If the ModelItem "insertBefore" is null or empty
380     * @throws UndefinedItemPathException If the path to the modelItem "insertBefore" is undefined
381     */
382    public static void insertItemBefore(ViewItemAccessor viewItemAccessor, ViewItem viewItemToInsert, String insertBefore)  throws IllegalArgumentException, UndefinedItemPathException
383    {
384        insertItemAfterOrBefore(viewItemAccessor, viewItemToInsert, insertBefore, InsertMode.BEFORE);
385    }
386
387    /**
388     * Insert the given {@link ViewItem} to the view before or after the item at the given path
389     * @param viewItemAccessor The ViewItemAccessor in which to insert the item
390     * @param itemToInsert The view item to insert
391     * @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
392     * @param insertMode The mode of insertion (before or after)
393     * @throws IllegalArgumentException If the ModelItem "insertAfterOrBefore" is null, empty, or is a path
394     * @throws UndefinedItemPathException If the path to the modelItem "insertAfterOrBefore" is undefined
395     */
396    public static void insertItemAfterOrBefore(ViewItemAccessor viewItemAccessor, ViewItem itemToInsert, String insertAfterOrBefore, InsertMode insertMode) throws IllegalArgumentException, UndefinedItemPathException
397    {
398        // Checks that the given item path is not empty
399        if (StringUtils.isBlank(insertAfterOrBefore))
400        {
401            throw new IllegalArgumentException("Unable to insert view item " + itemToInsert.getName() + " " + insertMode.toString() + " the view item with the given name. This name is empty.");
402        }
403        
404        // Checks that the given item path is not a path
405        if (insertAfterOrBefore.contains(ModelItem.ITEM_PATH_SEPARATOR))
406        {
407            throw new IllegalArgumentException("Unable to insert view item " + itemToInsert.getName() + " " + insertMode.toString() + " the view item with the name '" + insertAfterOrBefore + "'. This name is a path.");
408        }
409        
410        if (!_doInsertItemAfterOrBefore(viewItemAccessor, itemToInsert, insertAfterOrBefore, insertMode))
411        {
412            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) + "'");
413        }
414    }
415    
416    private static boolean _doInsertItemAfterOrBefore(ViewItemAccessor viewItemAccessor, ViewItem itemToInsert, String insertAfterOrBefore, InsertMode insertMode)
417    {
418        List<ViewItem> viewItems = viewItemAccessor.getViewItems();
419        for (int indexInAccessor = 0; indexInAccessor < viewItems.size(); indexInAccessor++)
420        {
421            ViewItem viewItem = viewItems.get(indexInAccessor);
422            if (insertAfterOrBefore.equals(viewItem.getName()))
423            {
424                // If the viewItemAccessor is a ModelViewItem, then check if the itemToInsert is a part of the viewItemAccessor's model
425                if (viewItemAccessor instanceof ModelViewItem modelViewItem && !((ModelItemAccessor) modelViewItem.getDefinition()).hasModelItem(itemToInsert.getName()))
426                {
427                    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.");
428                }
429                
430                // Insert the view item to insert, in the view
431                viewItemAccessor.insertViewItem(itemToInsert, indexInAccessor + insertMode.getPositionFromReferencedViewItem());
432                return true;
433            }
434        }
435        
436        return false;
437    }
438    
439    private static String _getViewItemAccessorName(ViewItemAccessor viewItemAccessor)
440    {
441        return viewItemAccessor instanceof View view 
442                ? view.getName() 
443                : viewItemAccessor instanceof ViewItem viewItem
444                    ? viewItem.getName()
445                    : viewItemAccessor.toString();
446    }
447    
448    /**
449     * Retrieves the paths of all model items in the given {@link View}
450     * @param view the {@link View}
451     * @return the paths of all items
452     */
453    public static Set<String> getModelItemsPathsFromView(View view)
454    {
455        return _getModelItemsFromViewItemContainer(view).keySet();
456    }
457    
458    /**
459     * Retrieves all model items in the given {@link View}
460     * @param view the {@link View}
461     * @return all model items
462     */
463    public static Collection<ModelItem> getModelItemsFromView(View view)
464    {
465        return _getModelItemsFromViewItemContainer(view).values();
466    }
467    
468    /**
469     * Retrieves all model items in the given {@link ViewItemContainer}, indexed by their paths
470     * @param viewItemContainer the {@link ViewItemContainer}
471     * @return all model items
472     */
473    private static Map<String, ModelItem> _getModelItemsFromViewItemContainer(ViewItemContainer viewItemContainer)
474    {
475        Map<String, ModelItem> result = new HashMap<>();
476        
477        for (ViewItem viewItem : viewItemContainer.getViewItems())
478        {
479            if (viewItem instanceof ModelViewItem)
480            {
481                ModelItem modelItem = ((ModelViewItem) viewItem).getDefinition();
482                result.put(modelItem.getPath(), modelItem);
483            }
484            
485            if (viewItem instanceof ViewItemContainer)
486            {
487                result.putAll(_getModelItemsFromViewItemContainer((ViewItemContainer) viewItem));
488            }
489        }
490        
491        return result;
492    }
493    
494    /**
495     * Checks if all the items of the given view are present only once in the view
496     * Children of accessors that are not containers are not taken into account
497     * @param view the view to check
498     * @return <code>true</code> if all items are presents only once, <code>false</code> otherwise
499     */
500    public static boolean areItemsPresentsOnlyOnce(View view)
501    {
502        return _areItemsPresentsOnlyOnce(view, new HashSet<>());
503    }
504    
505    private static boolean _areItemsPresentsOnlyOnce(ViewItemContainer containerToCheck, Set<String> paths)
506    {
507        for (ViewItem viewItem : containerToCheck.getViewItems())
508        {
509            if (viewItem instanceof ModelViewItem)
510            {
511                String viewItemPath = ((ModelViewItem) viewItem).getDefinition().getPath();
512                
513                if (paths.contains(viewItemPath))
514                {
515                    return false;
516                }
517                else
518                {
519                    paths.add(viewItemPath);
520                }
521            }
522
523            if (viewItem instanceof ViewItemContainer && !_areItemsPresentsOnlyOnce((ViewItemContainer) viewItem, paths))
524            {
525                return false;
526            }
527        }
528        
529        return true;
530    }
531    
532    /**
533     * Retrieves a View corresponding to the given one, avoiding the view items below the {@link ViewItemAccessor}s that are not {@link ViewItemContainer}s
534     * @param originalView the view to truncate
535     * @return the truncated view
536     */
537    public static View getTruncatedView(View originalView)
538    {
539        View view = new View();
540        view.addViewItems(_copyItemsForTruncatedView(originalView));
541        return view;
542    }
543    
544    private static List<ViewItem> _copyItemsForTruncatedView(ViewItemContainer currentOrigContainer)
545    {
546        List<ViewItem> copies = new ArrayList<>();
547        for (ViewItem origChild : currentOrigContainer.getViewItems())
548        {
549            // do not copy non editable item for the truncated view
550            if (_isEditable(origChild))
551            {
552                ViewItem destChild = origChild.createInstance();
553                origChild.copyTo(destChild);
554                
555                // Get children of containers, not accessors for the truncated view
556                if (origChild instanceof ViewItemContainer)
557                {
558                    ((ViewItemContainer) destChild).addViewItems(_copyItemsForTruncatedView((ViewItemContainer) origChild));
559                }
560                
561                copies.add(destChild);
562            }
563        }
564        
565        return copies;
566    }
567    
568    private static boolean _isEditable(ViewItem viewItem)
569    {
570        return viewItem instanceof ViewElement viewElement ? viewElement.getDefinition().isEditable() : true;
571    }
572    
573    /**
574     * Retrieves a View corresponding to the given one, where items that appears several times are merged
575     * @param originalView the view to merge
576     * @return the merged view
577     */
578    public static View mergeDuplicatedItems(View originalView)
579    {
580        View view = new View();
581        
582        _mergeDuplicatedItems(originalView, view, view, StringUtils.EMPTY);
583        
584        return view;
585    }
586    
587    private static void _mergeDuplicatedItems(ViewItemAccessor currentOrigAccessor, ViewItemAccessor currentDestAccessor, View destinationView, String accessorPath)
588    {
589        for (ViewItem origChild : currentOrigAccessor.getViewItems())
590        {
591            if (origChild instanceof ModelViewItem)
592            {
593                String itemPath = StringUtils.isNotEmpty(accessorPath) ? accessorPath + ModelItem.ITEM_PATH_SEPARATOR + origChild.getName() : origChild.getName();
594                if (ViewHelper.hasModelViewItem(destinationView, itemPath))
595                {
596                    // Ignore View elements, check view item accessors
597                    if (origChild instanceof ViewItemAccessor)
598                    {
599                        ViewItem destChild = ViewHelper.getModelViewItem(destinationView, itemPath);
600                        _mergeDuplicatedItems((ViewItemAccessor) origChild, (ViewItemAccessor) destChild, destinationView, itemPath);
601                    }
602                }
603                else
604                {
605                    ViewItem destChild = origChild.createInstance();
606                    currentDestAccessor.addViewItem(destChild);
607                    origChild.copyTo(destChild, destinationView, itemPath);
608                }
609            }
610            else
611            {
612                ViewItem destChild = origChild.createInstance();
613                currentDestAccessor.addViewItem(destChild);
614                origChild.copyTo(destChild);
615                _mergeDuplicatedItems((ViewItemAccessor) origChild, (ViewItemAccessor) destChild, destinationView, accessorPath);
616            }
617        }
618    }
619    
620    /**
621     * Checks if there is a {@link ModelViewItem} in the {@link ViewItemAccessor} at the given path
622     * @param viewItemAccessor The accessor of the view items
623     * @param itemPath The path of the item to check
624     * @return <code>true</code> if there is a model view item at the given path, <code>false</code> otherwise
625     */
626    public static boolean hasModelViewItem(ViewItemAccessor viewItemAccessor, String itemPath)
627    {
628        try
629        {
630            getModelViewItem(viewItemAccessor, itemPath);
631            // The model item can never be null. If no excpetion has been thrown, the there is a model item at this path
632            return true;
633        }
634        catch (UndefinedItemPathException | BadItemTypeException e)
635        {
636            return false;
637        }
638    }
639    
640    /**
641     * Converts the given view items as a JSON map
642     * @param viewItems the view items to convert
643     * @param context the context of the items' definitions
644     * @return The view items as a JSON map
645     * @throws ProcessingException If an error occurs when converting the view items
646     */
647    public static Map<String, Object> viewItemsToJSON(List<ViewItem> viewItems, DefinitionContext context) throws ProcessingException
648    {
649        Map<String, Object> elements = new LinkedHashMap<>();
650        
651        for (ViewItem item : viewItems)
652        {
653            Map<String, Object> itemAsJSON = item.toJSON(context);
654
655            if (!itemAsJSON.isEmpty())
656            {
657                String itemUUID = item.getName();
658                if (StringUtils.isEmpty(itemUUID))
659                {
660                    itemUUID = UUID.randomUUID().toString();
661                }
662                
663                elements.put(itemUUID, itemAsJSON);
664            }
665        }
666        
667        return elements;
668    }
669    
670    /**
671     * Gets the {@link ModelViewItem} from the {@link ViewItemAccessor} at the given path.
672     * <br>Unlike {@link View#getModelViewItem(String)}, this method accepts a path and not only a name, allowing to traverse composites.
673     * @param viewItemAccessor The accessor of view items
674     * @param itemPath The path of the item to get
675     * @return The {@link ModelViewItem}. Can never be <code>null</code>
676     * @throws UndefinedItemPathException If one of the parts of the given path is undefined
677     * @throws BadItemTypeException If one of the parts of the given path is defined, but is not of the correct type
678     */
679    public static ModelViewItem getModelViewItem(ViewItemAccessor viewItemAccessor, String itemPath) throws UndefinedItemPathException, BadItemTypeException
680    {
681        return new ViewItemGetter(viewItemAccessor).getViewItem(itemPath);
682    }
683    
684    /**
685     * Gets the {@link ViewElement} from the {@link ViewItemAccessor} at the given path.
686     * <br>Unlike {@link View#getModelViewItem(String)}, this method accepts a path and not only a name, allowing to traverse composites.
687     * @param viewItemAccessor The accessor of view items
688     * @param itemPath The path of the item to get
689     * @return The {@link ViewElement}. Can never be <code>null</code>
690     * @throws UndefinedItemPathException If one of the parts of the given path is undefined
691     * @throws BadItemTypeException If one of the parts of the given path is defined, but is not of the correct type
692     */
693    public static ViewElement getViewElement(ViewItemAccessor viewItemAccessor, String itemPath) throws UndefinedItemPathException, BadItemTypeException
694    {
695        return new ViewItemGetter(viewItemAccessor).getModelViewItem(itemPath);
696    }
697    
698    /**
699     * Gets the {@link ModelViewItemGroup} from the {@link ViewItemAccessor} at the given path.
700     * <br>Unlike {@link View#getModelViewItem(String)}, this method accepts a path and not only a name, allowing to traverse composites.
701     * @param viewItemAccessor The accessor of view items
702     * @param itemPath The path of the container to get
703     * @return The {@link ModelViewItemGroup}. Can never be <code>null</code>
704     * @throws UndefinedItemPathException If one of the parts of the given path is undefined
705     * @throws BadItemTypeException If one of the parts of the given path is defined, but is not of the correct type
706     */
707    public static ModelViewItemGroup getModelViewItemGroup(ViewItemAccessor viewItemAccessor, String itemPath) throws UndefinedItemPathException, BadItemTypeException
708    {
709        return new ViewItemGetter(viewItemAccessor).getViewItemContainer(itemPath);
710    }
711    
712    private static class ViewItemGetter
713    {
714        private final ViewItemAccessor _viewItemAccessor;
715        private String _wholePath;
716        
717        ViewItemGetter(ViewItemAccessor viewItemAccessor)
718        {
719            _viewItemAccessor = viewItemAccessor;
720        }
721        
722        ModelViewItem getViewItem(String wholePath) throws UndefinedItemPathException, BadItemTypeException
723        {
724            _wholePath = wholePath;
725            BiFunction<ViewItemAccessor, String, ModelViewItem> lastPartGetter = (viewItemAccessor, lastPart) -> _getDirectViewItem(viewItemAccessor, lastPart, ModelViewItem.class);
726            return _getModelViewItem(lastPartGetter);
727        }
728        
729        ViewElement getModelViewItem(String wholePath) throws UndefinedItemPathException, BadItemTypeException
730        {
731            _wholePath = wholePath;
732            BiFunction<ViewItemAccessor, String, ViewElement> lastPartGetter = (viewItemAccessor, lastPart) -> _getDirectViewItem(viewItemAccessor, lastPart, ViewElement.class);
733            return _getModelViewItem(lastPartGetter);
734        }
735        
736        ModelViewItemGroup getViewItemContainer(String wholePath) throws UndefinedItemPathException, BadItemTypeException
737        {
738            _wholePath = wholePath;
739            BiFunction<ViewItemAccessor, String, ModelViewItemGroup> lastPartGetter = (viewItemAccessor, lastPart) -> _getDirectModelViewItemGroup(viewItemAccessor, lastPart);
740            return _getModelViewItem(lastPartGetter);
741        }
742        
743        private <T> T _getModelViewItem(BiFunction<ViewItemAccessor, String, T> lastPartGetter) throws UndefinedItemPathException, BadItemTypeException
744        {
745            Deque<String> parts = new ArrayDeque<>(Arrays.asList(_wholePath.split(ModelItem.ITEM_PATH_SEPARATOR)));
746            String lastPart = parts.removeLast();
747            ViewItemAccessor currentViewItemAccessor = _viewItemAccessor;
748            while (!parts.isEmpty())
749            {
750                String currentPart = parts.pop();
751                currentViewItemAccessor =  _getDirectViewItem(currentViewItemAccessor, currentPart, ViewItemAccessor.class);
752            }
753            
754            return lastPartGetter.apply(currentViewItemAccessor, lastPart);
755        }
756        
757        private ModelViewItemGroup _getDirectModelViewItemGroup(ViewItemAccessor viewItemAccessor, String itemName) throws UndefinedItemPathException, BadItemTypeException
758        {
759            return _getDirectViewItem(viewItemAccessor, itemName, ModelViewItemGroup.class);
760        }
761        
762        private <T> T _getDirectViewItem(ViewItemAccessor viewItemAccessor, String itemName, Class<T> resultClass) throws UndefinedItemPathException, BadItemTypeException
763        {
764            if (!viewItemAccessor.hasModelViewItem(itemName))
765            {
766                throw new UndefinedItemPathException("For path '" + _wholePath + "', the part '" + itemName + "' is not defined");
767            }
768            else
769            {
770                ModelViewItem modelViewItem = viewItemAccessor.getModelViewItem(itemName);
771                if (resultClass.isInstance(modelViewItem))
772                {
773                    return resultClass.cast(modelViewItem);
774                }
775                else
776                {
777                    throw new BadItemTypeException("For path '" + _wholePath + "', the part '" + itemName + "' does not point to a '" + resultClass + "' (got a '" + modelViewItem.getClass().getName() + "')");
778                }
779            }
780        }
781    }
782}