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.Arrays;
020import java.util.Collection;
021import java.util.Deque;
022import java.util.HashMap;
023import java.util.Map;
024import java.util.Optional;
025import java.util.Set;
026import java.util.function.BiFunction;
027import java.util.stream.Collectors;
028
029import org.apache.commons.lang3.StringUtils;
030
031import org.ametys.runtime.model.exception.BadItemTypeException;
032import org.ametys.runtime.model.exception.UndefinedItemPathException;
033
034/**
035 * Helper class for views
036 */
037public final class ViewHelper
038{
039    private ViewHelper()
040    {
041        // Empty constructor
042    }
043    
044    /**
045     * Add the items of the view item container to include if they are not already present in the current view item container or in the reference view
046     * @param currentContainer the current container
047     * @param containerToInclude the container to include
048     * @param referenceView the reference view
049     */
050    public static void addViewContainerItems(ViewItemContainer currentContainer, ViewItemContainer containerToInclude, View referenceView)
051    {
052        for (ViewItem itemToCopy : containerToInclude.getViewItems())
053        {
054            if (itemToCopy instanceof SimpleViewItemGroup)
055            {
056                SimpleViewItemGroup groupToCopy = (SimpleViewItemGroup) itemToCopy;
057                SimpleViewItemGroup group = new SimpleViewItemGroup();
058                group.copyGroupItem(groupToCopy, referenceView);
059                
060                currentContainer.addViewItem(group);
061            }
062            else
063            {
064                String itemName = itemToCopy.getName();
065                if (!currentContainer.hasModelViewItem(itemName) && !referenceView.hasModelViewItem(itemName))
066                {
067                    currentContainer.addViewItem(itemToCopy);
068                }
069            }
070        }
071    }
072    
073    /**
074     * Creates a {@link ViewItemContainer} with the items of the given {@link ModelItemContainer}
075     * @param <T> Type of the {@link ViewItemContainer} to create ({@link View} or {@link ModelViewItemGroup})
076     * @param modelItemContainers the model item containers
077     * @return the created {@link ViewItemContainer}
078     * @throws IllegalArgumentException if the model item containers collection is empty
079     */
080    public static <T extends ViewItemContainer> T createViewItemContainer(Collection<? extends ModelItemContainer> modelItemContainers) throws IllegalArgumentException
081    {
082        T viewItemContainer = createEmptyViewItemContainer(modelItemContainers);
083        
084        for (ModelItemContainer modelItemContainer : modelItemContainers)
085        {
086            for (ModelItem modelItem : modelItemContainer.getModelItems())
087            {
088                addViewItem(modelItem.getName(), viewItemContainer, modelItemContainer);
089            }
090        }
091        
092        return viewItemContainer;
093    }
094    
095    /**
096     * Creates a {@link ViewItemContainer} with the given items
097     * @param <T> Type of the {@link ViewItemContainer} to create ({@link View} or {@link ModelViewItemGroup})
098     * @param modelItemContainers the model items containing items definitions
099     * @param itemPaths the paths of the items to put in the view item container
100     * @return the created {@link ViewItemContainer}
101     * @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
102     * @throws BadItemTypeException if a segment in a path (but not the last) does not represent a group item
103     */
104    public static <T extends ViewItemContainer> T createViewItemContainer(Collection<? extends ModelItemContainer> modelItemContainers, String... itemPaths) throws IllegalArgumentException, BadItemTypeException
105    {
106        T viewItemContainer = createEmptyViewItemContainer(modelItemContainers);
107        
108        for (String itemPath : itemPaths)
109        {
110            if (ModelHelper.hasModelItem(itemPath, modelItemContainers))
111            {
112                addViewItem(itemPath, viewItemContainer, modelItemContainers.toArray(new ModelItemContainer[modelItemContainers.size()]));
113            }
114            else
115            {
116                String modelIds = StringUtils.join(modelItemContainers.stream()
117                                                         .map(modelItemContainer -> _getModelItemContainerIdentifier(modelItemContainer))
118                                                         .collect(Collectors.toList()), ", ");
119                throw new IllegalArgumentException("Item '" + itemPath + "' not found in models: '" + modelIds + "'.");
120            }
121        }
122        
123        return viewItemContainer;
124    }
125    
126    /**
127     * Creates an empty {@link ViewItemContainer}
128     * The created container can be a {@link View} or a {@link ModelViewItemGroup}, according to the given {@link ModelItemContainer}s.
129     * @param <T> The type of the created container
130     * @param modelItemContainers the model items containing items definitions  
131     * @return the created {@link ViewItemContainer}
132     * @throws IllegalArgumentException if the model item containers collection is empty
133     */
134    @SuppressWarnings("unchecked")
135    public static <T extends ViewItemContainer> T createEmptyViewItemContainer(Collection<? extends ModelItemContainer> modelItemContainers) throws IllegalArgumentException
136    {
137        if (modelItemContainers.isEmpty())
138        {
139            throw new IllegalArgumentException("The model is needed to create a view item container");
140        }
141        else
142        {
143            ModelItemContainer firstModelItemContainer = modelItemContainers.iterator().next();
144            if (firstModelItemContainer instanceof ModelItemGroup)
145            {
146                ModelViewItemGroup viewItemContainer = new ModelViewItemGroup();
147                viewItemContainer.setDefinition((ModelItemGroup) firstModelItemContainer);
148                return (T) viewItemContainer;
149            }
150            else
151            {
152                return (T) new View();
153            }
154        }
155    }
156    
157    /**
158     * Add a view item in the given container
159     * @param relativePath path of the item to add
160     * @param viewItemContainer the view item container
161     * @param modelItemContainers the corresponding model item containers
162     * @throws IllegalArgumentException if the path is <code>null</code> or empty
163     * @throws UndefinedItemPathException if the path is not defined in the given model item containers
164     * @throws BadItemTypeException if a segment in a path (but not the last) does not represent a group item
165     */
166    @SuppressWarnings("unchecked")
167    public static void addViewItem(String relativePath, ViewItemContainer viewItemContainer, ModelItemContainer... modelItemContainers) throws IllegalArgumentException, UndefinedItemPathException, BadItemTypeException
168    {
169        int firstIndexOfItemPathSeparator = relativePath.indexOf(ModelItem.ITEM_PATH_SEPARATOR);
170        String fisrtPathSegment = firstIndexOfItemPathSeparator > -1 ? relativePath.substring(0, relativePath.indexOf(ModelItem.ITEM_PATH_SEPARATOR)) : relativePath;
171        
172        ModelItem modelItem = ModelHelper.getModelItem(fisrtPathSegment, Arrays.asList(modelItemContainers));
173        
174        // Find the view item in the current view item container
175        ModelViewItem viewItem = viewItemContainer.getModelViewItem(fisrtPathSegment);
176        if (viewItem == null)
177        {
178            // If the view item does not exist, then create it...
179            viewItem = modelItem instanceof ModelItemGroup ? new ModelViewItemGroup() : new ViewElement();
180            viewItem.setDefinition(modelItem);
181            
182            // ... and add it to the current view item container
183            viewItemContainer.addViewItem(viewItem);
184        }
185        
186        if (viewItem instanceof ViewItemContainer)
187        {
188            // If the view item is a container, the model item is a container too
189            ModelItemContainer newModelItemContainer = (ModelItemContainer) modelItem;
190            
191            if (firstIndexOfItemPathSeparator > -1)
192            {
193                // Only the first segment of the path has been processed, now recursively process the next ones
194                String subPath = relativePath.substring(firstIndexOfItemPathSeparator + 1);
195                addViewItem(subPath, (ViewItemContainer) viewItem, newModelItemContainer);
196            }
197            else
198            {
199                // The last segment is a group, add all its children in the current view item container
200                for (ModelItem child : newModelItemContainer.getModelItems())
201                {
202                    String newRelativePath = StringUtils.removeStart(child.getPath(), modelItem.getPath() + ModelItem.ITEM_PATH_SEPARATOR);
203                    addViewItem(newRelativePath, (ViewItemContainer) viewItem, newModelItemContainer);
204                }
205            }
206        }
207        else if (firstIndexOfItemPathSeparator > -1)
208        {
209            // The path has several segments but the first one is not a view item container
210            throw new BadItemTypeException("The segments inside the items' paths can only refer to a group. The path '" + relativePath + "' refers to the item '" + modelItem.getPath() + "' that is not a group");
211        }
212    }
213    
214    private static Optional<String> _getModelItemContainerIdentifier(ModelItemContainer container)
215    {
216        if (container instanceof Model)
217        {
218            return Optional.ofNullable(((Model) container).getId());
219        }
220        else if (container instanceof ModelItem)
221        {
222            return Optional.ofNullable(((ModelItem) container).getName());
223        }
224        else
225        {
226            return Optional.empty();
227        }
228    }
229    
230    /**
231     * Retrieves the paths of all model items in the given {@link View}
232     * @param view the {@link View}
233     * @return the paths of all items
234     */
235    public static Set<String> getModelItemsPathsFromView(View view)
236    {
237        return _getModelItemsFromViewItemContainer(view).keySet();
238    }
239    
240    /**
241     * Retrieves all model items in the given {@link View}
242     * @param view the {@link View}
243     * @return all model items
244     */
245    public static Collection<ModelItem> getModelItemsFromView(View view)
246    {
247        return _getModelItemsFromViewItemContainer(view).values();
248    }
249    
250    /**
251     * Retrieves all model items in the given {@link ViewItemContainer}, indexed by their paths
252     * @param viewItemContainer the {@link ViewItemContainer}
253     * @return all model items
254     */
255    private static Map<String, ModelItem> _getModelItemsFromViewItemContainer(ViewItemContainer viewItemContainer)
256    {
257        Map<String, ModelItem> result = new HashMap<>();
258        
259        for (ViewItem viewItem : viewItemContainer.getViewItems())
260        {
261            if (viewItem instanceof ModelViewItem)
262            {
263                ModelItem modelItem = ((ModelViewItem) viewItem).getDefinition();
264                result.put(modelItem.getPath(), modelItem);
265            }
266            
267            if (viewItem instanceof ViewItemContainer)
268            {
269                result.putAll(_getModelItemsFromViewItemContainer((ViewItemContainer) viewItem));
270            }
271        }
272        
273        return result;
274    }
275    
276    /**
277     * Gets the {@link ModelViewItem} from the {@link ViewItemContainer} at the given path.
278     * <br>Unlike {@link View#getModelViewItem(String)}, this method accepts a path and not only a name, allowing to traverse composites.
279     * @param itemContainer The container of view items
280     * @param itemPath The path of the item to get
281     * @return The {@link ModelViewItem}. Can never be <code>null</code>
282     * @throws UndefinedItemPathException If one of the parts of the given path is undefined
283     * @throws BadItemTypeException If one of the parts of the given path is defined, but is not of the correct type
284     */
285    public static ModelViewItem getModelViewItem(ViewItemContainer itemContainer, String itemPath) throws UndefinedItemPathException, BadItemTypeException
286    {
287        return new ViewItemGetter(itemContainer).getViewItem(itemPath);
288    }
289    
290    /**
291     * Gets the {@link ViewElement} from the {@link ViewItemContainer} at the given path.
292     * <br>Unlike {@link View#getModelViewItem(String)}, this method accepts a path and not only a name, allowing to traverse composites.
293     * @param itemContainer The container of view items
294     * @param itemPath The path of the item to get
295     * @return The {@link ViewElement}. Can never be <code>null</code>
296     * @throws UndefinedItemPathException If one of the parts of the given path is undefined
297     * @throws BadItemTypeException If one of the parts of the given path is defined, but is not of the correct type
298     */
299    public static ViewElement getViewElement(ViewItemContainer itemContainer, String itemPath) throws UndefinedItemPathException, BadItemTypeException
300    {
301        return new ViewItemGetter(itemContainer).getModelViewItem(itemPath);
302    }
303    
304    /**
305     * Gets the {@link ModelViewItemGroup} from the {@link ViewItemContainer} at the given path.
306     * <br>Unlike {@link View#getModelViewItem(String)}, this method accepts a path and not only a name, allowing to traverse composites.
307     * @param itemContainer The container of view items
308     * @param itemPath The path of the container to get
309     * @return The {@link ModelViewItemGroup}. Can never be <code>null</code>
310     * @throws UndefinedItemPathException If one of the parts of the given path is undefined
311     * @throws BadItemTypeException If one of the parts of the given path is defined, but is not of the correct type
312     */
313    public static ModelViewItemGroup getModelViewItemGroup(ViewItemContainer itemContainer, String itemPath) throws UndefinedItemPathException, BadItemTypeException
314    {
315        return new ViewItemGetter(itemContainer).getViewItemContainer(itemPath);
316    }
317    
318    private static class ViewItemGetter
319    {
320        private final ViewItemContainer _itemContainer;
321        private String _wholePath;
322        
323        ViewItemGetter(ViewItemContainer view)
324        {
325            _itemContainer = view;
326        }
327        
328        ModelViewItem getViewItem(String wholePath) throws UndefinedItemPathException, BadItemTypeException
329        {
330            _wholePath = wholePath;
331            BiFunction<ViewItemContainer, String, ModelViewItem> lastPartGetter = (viewItemContainer, lastPart) -> _getDirectViewItem(viewItemContainer, lastPart, ModelViewItem.class);
332            return _getModelViewItem(lastPartGetter);
333        }
334        
335        ViewElement getModelViewItem(String wholePath) throws UndefinedItemPathException, BadItemTypeException
336        {
337            _wholePath = wholePath;
338            BiFunction<ViewItemContainer, String, ViewElement> lastPartGetter = (viewItemContainer, lastPart) -> _getDirectViewItem(viewItemContainer, lastPart, ViewElement.class);
339            return _getModelViewItem(lastPartGetter);
340        }
341        
342        ModelViewItemGroup getViewItemContainer(String wholePath) throws UndefinedItemPathException, BadItemTypeException
343        {
344            _wholePath = wholePath;
345            BiFunction<ViewItemContainer, String, ModelViewItemGroup> lastPartGetter = (viewItemContainer, lastPart) -> _getDirectViewItemContainer(viewItemContainer, lastPart);
346            return _getModelViewItem(lastPartGetter);
347        }
348        
349        private <T> T _getModelViewItem(BiFunction<ViewItemContainer, String, T> lastPartGetter) throws UndefinedItemPathException, BadItemTypeException
350        {
351            Deque<String> parts = new ArrayDeque<>(Arrays.asList(_wholePath.split(ModelItem.ITEM_PATH_SEPARATOR)));
352            String lastPart = parts.removeLast();
353            ViewItemContainer currentViewItemContainer = _itemContainer;
354            while (!parts.isEmpty())
355            {
356                String currentPart = parts.pop();
357                currentViewItemContainer =  _getDirectViewItemContainer(currentViewItemContainer, currentPart);
358            }
359            
360            return lastPartGetter.apply(currentViewItemContainer, lastPart);
361        }
362        
363        private ModelViewItemGroup _getDirectViewItemContainer(ViewItemContainer viewItemContainer, String itemName) throws UndefinedItemPathException, BadItemTypeException
364        {
365            return _getDirectViewItem(viewItemContainer, itemName, ModelViewItemGroup.class);
366        }
367        
368        private <T> T _getDirectViewItem(ViewItemContainer viewItemContainer, String itemName, Class<T> resultClass) throws UndefinedItemPathException, BadItemTypeException
369        {
370            if (!viewItemContainer.hasModelViewItem(itemName))
371            {
372                throw new UndefinedItemPathException("For path '" + _wholePath + "', the part '" + itemName + "' is not defined");
373            }
374            else
375            {
376                ModelViewItem modelViewItem = viewItemContainer.getModelViewItem(itemName);
377                if (resultClass.isInstance(modelViewItem))
378                {
379                    return resultClass.cast(modelViewItem);
380                }
381                else
382                {
383                    throw new BadItemTypeException("For path '" + _wholePath + "', the part '" + itemName + "' does not point to a '" + resultClass + "' (got a '" + modelViewItem.getClass().getName() + "')");
384                }
385            }
386        }
387    }
388}