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     * 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
052     * @param currentAccessor the current accessor
053     * @param accessorToInclude the accessor to include
054     * @param referenceView the reference view
055     * @param accessorPath the path of the accessor in the reference view
056     */
057    public static void addViewAccessorItems(ViewItemAccessor currentAccessor, ViewItemAccessor accessorToInclude, View referenceView, String accessorPath)
058    {
059        for (ViewItem itemToInclude : accessorToInclude.getViewItems())
060        {
061            String itemPath = accessorPath;
062            if (itemToInclude instanceof ModelViewItem)
063            {
064                itemPath += StringUtils.isNotEmpty(accessorPath) ? ModelItem.ITEM_PATH_SEPARATOR + itemToInclude.getName() : itemToInclude.getName();
065            }
066
067            if (!(itemToInclude instanceof ModelViewItem) || !referenceView.hasModelViewItem((ModelViewItem) itemToInclude, StringUtils.EMPTY, itemPath))
068            {
069                ViewItem copy = itemToInclude.createInstance();
070                currentAccessor.addViewItem(copy);
071                itemToInclude.copyTo(copy, referenceView, itemPath);
072            }
073        }
074    }
075    
076    /**
077     * Creates a {@link ViewItemAccessor} with the items of the given {@link ModelItemAccessor}
078     * @param <T> Type of the {@link ViewItemAccessor} to create ({@link View}, {@link ModelViewItemGroup} or {@link ViewElementAccessor})
079     * @param modelItemAccessors the model item accessors
080     * @return the created {@link ViewItemAccessor}
081     * @throws IllegalArgumentException if the model item accessors collection is empty
082     */
083    public static <T extends ViewItemAccessor> T createViewItemAccessor(Collection<? extends ModelItemAccessor> modelItemAccessors) throws IllegalArgumentException
084    {
085        T viewItemAccessor = createEmptyViewItemAccessor(modelItemAccessors);
086        
087        for (ModelItemAccessor modelItemAccessor : modelItemAccessors)
088        {
089            for (ModelItem modelItem : modelItemAccessor.getModelItems())
090            {
091                if (!viewItemAccessor.hasModelViewItem(modelItem.getName()))
092                {
093                    addViewItem(modelItem.getName(), viewItemAccessor, modelItemAccessor);
094                }
095            }
096        }
097        
098        return viewItemAccessor;
099    }
100    
101    /**
102     * Creates a {@link ViewItemAccessor} with the given items
103     * @param <T> Type of the {@link ViewItemAccessor} to create ({@link View}, {@link ModelViewItemGroup} or {@link ViewElementAccessor})
104     * @param modelItemAccessors the model items accessing the items definitions
105     * @param itemPaths the paths of the items to put in the view item accessor
106     * @return the created {@link ViewItemAccessor}
107     * @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
108     * @throws BadItemTypeException if a segment in a path (but not the last) does not represent a group item
109     */
110    public static <T extends ViewItemAccessor> T createViewItemAccessor(Collection<? extends ModelItemAccessor> modelItemAccessors, String... itemPaths) throws IllegalArgumentException, BadItemTypeException
111    {
112        T viewItemContainer = createEmptyViewItemAccessor(modelItemAccessors);
113        
114        for (String itemPath : itemPaths)
115        {
116            if (ModelHelper.hasModelItem(itemPath, modelItemAccessors))
117            {
118                addViewItem(itemPath, viewItemContainer, modelItemAccessors.toArray(new ModelItemAccessor[modelItemAccessors.size()]));
119            }
120            else
121            {
122                String modelIds = StringUtils.join(modelItemAccessors.stream()
123                                                         .map(modelItemContainer -> _getModelItemAccessorIdentifier(modelItemContainer))
124                                                         .collect(Collectors.toList()), ", ");
125                throw new IllegalArgumentException("Item '" + itemPath + "' not found in models: '" + modelIds + "'.");
126            }
127        }
128        
129        return viewItemContainer;
130    }
131    
132    /**
133     * Creates an empty {@link ViewItemAccessor}
134     * The created container can be a {@link View}, a {@link ModelViewItemGroup} or a {@link ViewElementAccessor}, according to the given {@link ModelItemAccessor}s.
135     * @param <T> The type of the created accessor
136     * @param modelItemAccessors the model items accessing the items definitions  
137     * @return the created {@link ViewItemAccessor}
138     * @throws IllegalArgumentException if the model item accessors collection is empty
139     */
140    @SuppressWarnings("unchecked")
141    public static <T extends ViewItemAccessor> T createEmptyViewItemAccessor(Collection<? extends ModelItemAccessor> modelItemAccessors) throws IllegalArgumentException
142    {
143        if (modelItemAccessors.isEmpty())
144        {
145            throw new IllegalArgumentException("The model is needed to create a view item container");
146        }
147        else
148        {
149            ModelItemAccessor firstModelItemAccessor = modelItemAccessors.iterator().next();
150            if (firstModelItemAccessor instanceof ElementDefinition)
151            {
152                ViewElementAccessor viewItemAccesor = new ViewElementAccessor();
153                viewItemAccesor.setDefinition((ElementDefinition) firstModelItemAccessor);
154                return (T) viewItemAccesor;
155            }
156            else if (firstModelItemAccessor instanceof ModelItemGroup)
157            {
158                ModelViewItemGroup viewItemAccessor = new ModelViewItemGroup();
159                viewItemAccessor.setDefinition((ModelItemGroup) firstModelItemAccessor);
160                return (T) viewItemAccessor;
161            }
162            else
163            {
164                return (T) new View();
165            }
166        }
167    }
168    
169    /**
170     * Add a view item in the given accessor
171     * @param relativePath path of the item to add
172     * @param viewItemAccessor the view item accessor
173     * @param modelItemAccessors the corresponding model item accessors
174     * @throws IllegalArgumentException if the path is <code>null</code> or empty
175     * @throws UndefinedItemPathException if the path is not defined in the given model item accessors
176     * @throws BadItemTypeException if a segment in a path (but not the last) does not represent a group item
177     */
178    @SuppressWarnings("unchecked")
179    public static void addViewItem(String relativePath, ViewItemAccessor viewItemAccessor, ModelItemAccessor... modelItemAccessors) throws IllegalArgumentException, UndefinedItemPathException, BadItemTypeException
180    {
181        int firstIndexOfItemPathSeparator = relativePath.indexOf(ModelItem.ITEM_PATH_SEPARATOR);
182        String firstPathSegment = firstIndexOfItemPathSeparator > -1 ? relativePath.substring(0, relativePath.indexOf(ModelItem.ITEM_PATH_SEPARATOR)) : relativePath;
183        
184        ModelItem modelItem = ModelHelper.getModelItem(firstPathSegment, Arrays.asList(modelItemAccessors));
185        
186        // Create the view item and add it to the current view item container
187        ModelViewItem viewItem = null;
188        if (modelItem instanceof ModelItemAccessor)
189        {
190            if (viewItemAccessor.hasModelViewItem(firstPathSegment))
191            {
192                viewItem = viewItemAccessor.getModelViewItem(firstPathSegment);
193            }
194            else
195            {
196                if (modelItem instanceof ModelItemGroup)
197                {
198                    viewItem = new ModelViewItemGroup();
199                }
200                else
201                {
202                    viewItem = new ViewElementAccessor();
203                }
204                viewItem.setDefinition(modelItem);
205                viewItemAccessor.addViewItem(viewItem);
206            }
207        }
208        else
209        {
210            viewItem = new ViewElement();
211            viewItem.setDefinition(modelItem);
212            viewItemAccessor.addViewItem(viewItem);
213        }
214
215        if (firstIndexOfItemPathSeparator > -1)
216        {
217            if (modelItem instanceof ModelItemAccessor)
218            {
219                // Only the first segment of the path has been processed, now recursively process the next ones
220                String subPath = relativePath.substring(firstIndexOfItemPathSeparator + 1);
221                addViewItem(subPath, (ViewItemAccessor) viewItem, (ModelItemAccessor) modelItem);
222            }
223            else
224            {
225                // The path has several segments but the first one is not a view item accessor
226                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");
227            }
228        }
229        else if (modelItem instanceof ModelItemContainer)
230        {
231            // The last segment is a container, add all its children in the current view item container
232            for (ModelItem child : ((ModelItemContainer) modelItem).getModelItems())
233            {
234                String newRelativePath = StringUtils.removeStart(child.getPath(), modelItem.getPath() + ModelItem.ITEM_PATH_SEPARATOR);
235                addViewItem(newRelativePath, (ViewItemAccessor) viewItem, (ModelItemContainer) modelItem);
236            }
237        }
238    }
239    
240    private static Optional<String> _getModelItemAccessorIdentifier(ModelItemAccessor accessor)
241    {
242        if (accessor instanceof Model)
243        {
244            return Optional.ofNullable(((Model) accessor).getId());
245        }
246        else if (accessor instanceof ModelItem)
247        {
248            return Optional.ofNullable(((ModelItem) accessor).getName());
249        }
250        else
251        {
252            return Optional.empty();
253        }
254    }
255    
256    /**
257     * Retrieves the paths of all model items in the given {@link View}
258     * @param view the {@link View}
259     * @return the paths of all items
260     */
261    public static Set<String> getModelItemsPathsFromView(View view)
262    {
263        return _getModelItemsFromViewItemContainer(view).keySet();
264    }
265    
266    /**
267     * Retrieves all model items in the given {@link View}
268     * @param view the {@link View}
269     * @return all model items
270     */
271    public static Collection<ModelItem> getModelItemsFromView(View view)
272    {
273        return _getModelItemsFromViewItemContainer(view).values();
274    }
275    
276    /**
277     * Retrieves all model items in the given {@link ViewItemContainer}, indexed by their paths
278     * @param viewItemContainer the {@link ViewItemContainer}
279     * @return all model items
280     */
281    private static Map<String, ModelItem> _getModelItemsFromViewItemContainer(ViewItemContainer viewItemContainer)
282    {
283        Map<String, ModelItem> result = new HashMap<>();
284        
285        for (ViewItem viewItem : viewItemContainer.getViewItems())
286        {
287            if (viewItem instanceof ModelViewItem)
288            {
289                ModelItem modelItem = ((ModelViewItem) viewItem).getDefinition();
290                result.put(modelItem.getPath(), modelItem);
291            }
292            
293            if (viewItem instanceof ViewItemContainer)
294            {
295                result.putAll(_getModelItemsFromViewItemContainer((ViewItemContainer) viewItem));
296            }
297        }
298        
299        return result;
300    }
301    
302    /**
303     * Checks if all the items of the given view are present only once in the view
304     * Children of accessors that are not containers are not taken into account
305     * @param view the view to check
306     * @return <code>true</code> if all items are presents only once, <code>false</code> otherwise
307     */
308    public static boolean areItemsPresentsOnlyOnce(View view)
309    {
310        return _areItemsPresentsOnlyOnce(view, new HashSet<>());
311    }
312    
313    private static boolean _areItemsPresentsOnlyOnce(ViewItemContainer containerToCheck, Set<String> paths)
314    {
315        for (ViewItem viewItem : containerToCheck.getViewItems())
316        {
317            if (viewItem instanceof ModelViewItem)
318            {
319                String viewItemPath = ((ModelViewItem) viewItem).getDefinition().getPath();
320                
321                if (paths.contains(viewItemPath))
322                {
323                    return false;
324                }
325                else
326                {
327                    paths.add(viewItemPath);
328                }
329            }
330
331            if (viewItem instanceof ViewItemContainer && !_areItemsPresentsOnlyOnce((ViewItemContainer) viewItem, paths))
332            {
333                return false;
334            }
335        }
336        
337        return true;
338    }
339    
340    /**
341     * Retrieves a View corresponding to the given one, avoiding the view items below the {@link ViewItemAccessor}s that are not {@link ViewItemContainer}s
342     * @param originalView the view to truncate
343     * @return the truncated view
344     */
345    public static View getTruncatedView(View originalView)
346    {
347        View view = new View();
348        view.addViewItems(_copyItems(originalView));
349        return view;
350    }
351    
352    private static List<ViewItem> _copyItems(ViewItemContainer currentOrigContainer)
353    {
354        List<ViewItem> copies = new ArrayList<>();
355        for (ViewItem origChild : currentOrigContainer.getViewItems())
356        {
357            ViewItem destChild = origChild.createInstance();
358            origChild.copyTo(destChild);
359            
360            if (origChild instanceof ViewItemContainer)
361            {
362                ((ViewItemContainer) destChild).addViewItems(_copyItems((ViewItemContainer) origChild));
363            }
364            
365            copies.add(destChild);
366        }
367        
368        return copies;
369    }
370    
371    /**
372     * Retrieves a View corresponding to the given one, where items that appears several times are merged
373     * @param originalView the view to merge
374     * @return the merged view
375     */
376    public static View mergeDuplicatedItems(View originalView)
377    {
378        View view = new View();
379        
380        _mergeDuplicatedItems(originalView, view, view, StringUtils.EMPTY);
381        
382        return view;
383    }
384    
385    private static void _mergeDuplicatedItems(ViewItemAccessor currentOrigAccessor, ViewItemAccessor currentDestAccessor, View destinationView, String accessorPath)
386    {
387        for (ViewItem origChild : currentOrigAccessor.getViewItems())
388        {
389            if (origChild instanceof ModelViewItem)
390            {
391                String itemPath = StringUtils.isNotEmpty(accessorPath) ? accessorPath + ModelItem.ITEM_PATH_SEPARATOR + origChild.getName() : origChild.getName();
392                if (ViewHelper.hasModelViewItem(destinationView, itemPath))
393                {
394                    // Ignore View elements, check view item accessors
395                    if (origChild instanceof ViewItemAccessor)
396                    {
397                        ViewItem destChild = ViewHelper.getModelViewItem(destinationView, itemPath);
398                        _mergeDuplicatedItems((ViewItemAccessor) origChild, (ViewItemAccessor) destChild, destinationView, itemPath);
399                    }
400                }
401                else
402                {
403                    ViewItem destChild = origChild.createInstance();
404                    currentDestAccessor.addViewItem(destChild);
405                    origChild.copyTo(destChild, destinationView, itemPath);
406                }
407            }
408            else
409            {
410                ViewItem destChild = origChild.createInstance();
411                currentDestAccessor.addViewItem(destChild);
412                origChild.copyTo(destChild);
413                _mergeDuplicatedItems((ViewItemAccessor) origChild, (ViewItemAccessor) destChild, destinationView, accessorPath);
414            }
415        }
416    }
417    
418    /**
419     * Checks if there is a {@link ModelViewItem} in the {@link ViewItemAccessor} at the given path
420     * @param viewItemAccessor The accessor of the view items
421     * @param itemPath The path of the item to check
422     * @return <code>true</code> if there is a model view item at the given path, <code>false</code> otherwise
423     */
424    public static boolean hasModelViewItem(ViewItemAccessor viewItemAccessor, String itemPath)
425    {
426        try
427        {
428            getModelViewItem(viewItemAccessor, itemPath);
429            // The model item can never be null. If no excpetion has been thrown, the there is a model item at this path
430            return true;
431        }
432        catch (UndefinedItemPathException | BadItemTypeException e)
433        {
434            return false;
435        }
436    }
437    
438    /**
439     * Converts the given view items as a JSON map
440     * @param viewItems the view items to convert
441     * @param context the context of the items' definitions
442     * @return The view items as a JSON map
443     * @throws ProcessingException If an error occurs when converting the view items
444     */
445    public static Map<String, Object> viewItemsToJSON(List<ViewItem> viewItems, DefinitionContext context) throws ProcessingException
446    {
447        Map<String, Object> elements = new LinkedHashMap<>();
448        
449        for (ViewItem item : viewItems)
450        {
451            Map<String, Object> itemAsJSON = item.toJSON(context);
452
453            if (!itemAsJSON.isEmpty())
454            {
455                String itemUUID = item.getName();
456                if (StringUtils.isEmpty(itemUUID))
457                {
458                    itemUUID = UUID.randomUUID().toString();
459                }
460                
461                elements.put(itemUUID, itemAsJSON);
462            }
463        }
464        
465        return elements;
466    }
467    
468    /**
469     * Gets the {@link ModelViewItem} from the {@link ViewItemAccessor} at the given path.
470     * <br>Unlike {@link View#getModelViewItem(String)}, this method accepts a path and not only a name, allowing to traverse composites.
471     * @param viewItemAccessor The accessor of view items
472     * @param itemPath The path of the item to get
473     * @return The {@link ModelViewItem}. Can never be <code>null</code>
474     * @throws UndefinedItemPathException If one of the parts of the given path is undefined
475     * @throws BadItemTypeException If one of the parts of the given path is defined, but is not of the correct type
476     */
477    public static ModelViewItem getModelViewItem(ViewItemAccessor viewItemAccessor, String itemPath) throws UndefinedItemPathException, BadItemTypeException
478    {
479        return new ViewItemGetter(viewItemAccessor).getViewItem(itemPath);
480    }
481    
482    /**
483     * Gets the {@link ViewElement} from the {@link ViewItemAccessor} at the given path.
484     * <br>Unlike {@link View#getModelViewItem(String)}, this method accepts a path and not only a name, allowing to traverse composites.
485     * @param viewItemAccessor The accessor of view items
486     * @param itemPath The path of the item to get
487     * @return The {@link ViewElement}. Can never be <code>null</code>
488     * @throws UndefinedItemPathException If one of the parts of the given path is undefined
489     * @throws BadItemTypeException If one of the parts of the given path is defined, but is not of the correct type
490     */
491    public static ViewElement getViewElement(ViewItemAccessor viewItemAccessor, String itemPath) throws UndefinedItemPathException, BadItemTypeException
492    {
493        return new ViewItemGetter(viewItemAccessor).getModelViewItem(itemPath);
494    }
495    
496    /**
497     * Gets the {@link ModelViewItemGroup} from the {@link ViewItemAccessor} at the given path.
498     * <br>Unlike {@link View#getModelViewItem(String)}, this method accepts a path and not only a name, allowing to traverse composites.
499     * @param viewItemAccessor The accessor of view items
500     * @param itemPath The path of the container to get
501     * @return The {@link ModelViewItemGroup}. Can never be <code>null</code>
502     * @throws UndefinedItemPathException If one of the parts of the given path is undefined
503     * @throws BadItemTypeException If one of the parts of the given path is defined, but is not of the correct type
504     */
505    public static ModelViewItemGroup getModelViewItemGroup(ViewItemAccessor viewItemAccessor, String itemPath) throws UndefinedItemPathException, BadItemTypeException
506    {
507        return new ViewItemGetter(viewItemAccessor).getViewItemContainer(itemPath);
508    }
509    
510    private static class ViewItemGetter
511    {
512        private final ViewItemAccessor _viewItemAccessor;
513        private String _wholePath;
514        
515        ViewItemGetter(ViewItemAccessor viewItemAccessor)
516        {
517            _viewItemAccessor = viewItemAccessor;
518        }
519        
520        ModelViewItem getViewItem(String wholePath) throws UndefinedItemPathException, BadItemTypeException
521        {
522            _wholePath = wholePath;
523            BiFunction<ViewItemAccessor, String, ModelViewItem> lastPartGetter = (viewItemAccessor, lastPart) -> _getDirectViewItem(viewItemAccessor, lastPart, ModelViewItem.class);
524            return _getModelViewItem(lastPartGetter);
525        }
526        
527        ViewElement getModelViewItem(String wholePath) throws UndefinedItemPathException, BadItemTypeException
528        {
529            _wholePath = wholePath;
530            BiFunction<ViewItemAccessor, String, ViewElement> lastPartGetter = (viewItemAccessor, lastPart) -> _getDirectViewItem(viewItemAccessor, lastPart, ViewElement.class);
531            return _getModelViewItem(lastPartGetter);
532        }
533        
534        ModelViewItemGroup getViewItemContainer(String wholePath) throws UndefinedItemPathException, BadItemTypeException
535        {
536            _wholePath = wholePath;
537            BiFunction<ViewItemAccessor, String, ModelViewItemGroup> lastPartGetter = (viewItemAccessor, lastPart) -> _getDirectModelViewItemGroup(viewItemAccessor, lastPart);
538            return _getModelViewItem(lastPartGetter);
539        }
540        
541        private <T> T _getModelViewItem(BiFunction<ViewItemAccessor, String, T> lastPartGetter) throws UndefinedItemPathException, BadItemTypeException
542        {
543            Deque<String> parts = new ArrayDeque<>(Arrays.asList(_wholePath.split(ModelItem.ITEM_PATH_SEPARATOR)));
544            String lastPart = parts.removeLast();
545            ViewItemAccessor currentViewItemAccessor = _viewItemAccessor;
546            while (!parts.isEmpty())
547            {
548                String currentPart = parts.pop();
549                currentViewItemAccessor =  _getDirectViewItem(currentViewItemAccessor, currentPart, ViewItemAccessor.class);
550            }
551            
552            return lastPartGetter.apply(currentViewItemAccessor, lastPart);
553        }
554        
555        private ModelViewItemGroup _getDirectModelViewItemGroup(ViewItemAccessor viewItemAccessor, String itemName) throws UndefinedItemPathException, BadItemTypeException
556        {
557            return _getDirectViewItem(viewItemAccessor, itemName, ModelViewItemGroup.class);
558        }
559        
560        private <T> T _getDirectViewItem(ViewItemAccessor viewItemAccessor, String itemName, Class<T> resultClass) throws UndefinedItemPathException, BadItemTypeException
561        {
562            if (!viewItemAccessor.hasModelViewItem(itemName))
563            {
564                throw new UndefinedItemPathException("For path '" + _wholePath + "', the part '" + itemName + "' is not defined");
565            }
566            else
567            {
568                ModelViewItem modelViewItem = viewItemAccessor.getModelViewItem(itemName);
569                if (resultClass.isInstance(modelViewItem))
570                {
571                    return resultClass.cast(modelViewItem);
572                }
573                else
574                {
575                    throw new BadItemTypeException("For path '" + _wholePath + "', the part '" + itemName + "' does not point to a '" + resultClass + "' (got a '" + modelViewItem.getClass().getName() + "')");
576                }
577            }
578        }
579    }
580}