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