001/*
002 *  Copyright 2019 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.plugins.contentstree;
017
018import java.util.ArrayList;
019import java.util.Collections;
020import java.util.HashMap;
021import java.util.List;
022import java.util.Map;
023import java.util.Optional;
024
025import org.apache.avalon.framework.component.Component;
026import org.apache.avalon.framework.service.ServiceException;
027import org.apache.avalon.framework.service.ServiceManager;
028import org.apache.avalon.framework.service.Serviceable;
029import org.apache.commons.lang3.StringUtils;
030
031import org.ametys.cms.contenttype.ContentType;
032import org.ametys.cms.contenttype.ContentTypeExtensionPoint;
033import org.ametys.cms.contenttype.ContentTypesHelper;
034import org.ametys.cms.data.ContentValue;
035import org.ametys.cms.data.type.ModelItemTypeConstants;
036import org.ametys.cms.repository.Content;
037import org.ametys.cms.repository.ModifiableContent;
038import org.ametys.core.ui.Callable;
039import org.ametys.plugins.repository.AmetysObjectResolver;
040import org.ametys.plugins.repository.data.holder.ModelAwareDataHolder;
041import org.ametys.plugins.repository.data.holder.group.ModelAwareComposite;
042import org.ametys.plugins.repository.data.holder.group.ModelAwareRepeater;
043import org.ametys.plugins.repository.data.holder.group.ModelAwareRepeaterEntry;
044import org.ametys.plugins.repository.model.CompositeDefinition;
045import org.ametys.plugins.repository.model.RepeaterDefinition;
046import org.ametys.runtime.model.ModelItem;
047import org.ametys.runtime.model.ModelItemContainer;
048import org.ametys.runtime.plugin.component.AbstractLogEnabled;
049
050/**
051 * Helper for contents tree
052 *
053 */
054public class ContentsTreeHelper extends AbstractLogEnabled implements Component, Serviceable
055{
056    /** The Avalon role */
057    public static final String ROLE = ContentsTreeHelper.class.getName();
058    
059    /** The ametys object resolver instance */
060    protected AmetysObjectResolver _ametysResolver;
061    /** The tree configuration EP instance */
062    protected TreeExtensionPoint _treeExtensionPoint;
063    /** The content type EP instance */
064    protected ContentTypeExtensionPoint _contentTypesEP;
065    /** The content types helper instance */
066    protected ContentTypesHelper _contentTypesHelper;
067
068    @Override
069    public void service(ServiceManager smanager) throws ServiceException
070    {
071        _ametysResolver = (AmetysObjectResolver) smanager.lookup(AmetysObjectResolver.ROLE);
072        _treeExtensionPoint = (TreeExtensionPoint) smanager.lookup(TreeExtensionPoint.ROLE);
073        _contentTypesEP = (ContentTypeExtensionPoint) smanager.lookup(ContentTypeExtensionPoint.ROLE);
074        _contentTypesHelper = (ContentTypesHelper) smanager.lookup(ContentTypesHelper.ROLE);
075    }
076    
077    /**
078     * Determines if the content has children contents according the tree configuration
079     * @param content the root content
080     * @param treeConfiguration the tree configuration
081     * @return true if the content has children contents
082     */
083    public boolean hasChildrenContent(Content content, TreeConfiguration treeConfiguration)
084    {
085        return !getChildrenContent(content, treeConfiguration).isEmpty();
086    }
087    
088    /**
089     * Get the children contents according the tree configuration
090     * @param parentContent the root content
091     * @param treeConfiguration the tree configuration
092     * @return the children content for each child attributes
093     */
094    public Map<String, List<Content>> getChildrenContent(Content parentContent, TreeConfiguration treeConfiguration)
095    {
096        Map<String, List<Content>> childrenContent = new HashMap<>();
097        
098        // For each content type of the content
099        for (String contentTypeId : parentContent.getTypes())
100        {
101            // Loop over all possible elements for the tree
102            for (TreeConfigurationElements treeConfigurationElements : treeConfiguration.getElements())
103            {
104                // Check for a match between the element and the content type of the content
105                for (TreeConfigurationContentType treeConfigurationContentType : treeConfigurationElements.getContentTypesConfiguration())
106                {
107                    if (treeConfigurationContentType.getContentTypesIds().contains(contentTypeId))
108                    {
109                        ContentType contentType = _contentTypesEP.getExtension(contentTypeId);
110                        
111                        // Add all required children for this element
112                        for (TreeConfigurationElementsChild treeConfigurationElementsChild : treeConfigurationElements.getChildren())
113                        {
114                            if (treeConfigurationElementsChild instanceof AttributeTreeConfigurationElementsChild)
115                            {
116                                // Get the attribute
117                                Map<String, List<Content>> contents = _handleAttributeTreeConfigurationElementsChild(contentType, parentContent, (AttributeTreeConfigurationElementsChild) treeConfigurationElementsChild, treeConfiguration);
118                                _merge(childrenContent, contents);
119                            }
120                            else
121                            {
122                                throw new IllegalArgumentException("The child configuration element class <" + treeConfigurationElementsChild + "> is not supported in tree '" + treeConfiguration.getId() + "'");
123                            }
124                        }
125                    }
126                }
127                
128            }
129        }
130        
131        return childrenContent;
132    }
133    
134    /**
135     * Get the children contents according the tree configuration
136     * @param contentId the parent content
137     * @param treeId the tree configuration
138     * @return the children content
139     */
140    @Callable
141    public Map<String, Object> getChildrenContent(String contentId, String treeId)
142    {
143        TreeConfiguration treeConfiguration = _getTreeConfiguration(treeId);
144        Content parentContent = _getParentContent(contentId);
145        
146        Map<String, List<Content>> children = getChildrenContent(parentContent, treeConfiguration);
147        
148        Map<String, Object> infos = new HashMap<>();
149
150        List<Map<String, Object>> childrenInfos = new ArrayList<>();
151        infos.put("children", childrenInfos);
152
153        for (String attributePath : children.keySet())
154        {
155            for (Content childContent : children.get(attributePath))
156            {
157                Map<String, Object> childInfo = content2Json(childContent);
158                childInfo.put("metadataPath", attributePath);
159                
160                if (!hasChildrenContent(childContent, treeConfiguration))
161                {
162                    childInfo.put("children", Collections.EMPTY_LIST);
163                }
164
165                childrenInfos.add(childInfo);
166            }
167        }
168
169        return infos;
170    }
171    
172    /**
173     * Get the path of children content which match filter regexp
174     * @param parentContentId The id of content to start search
175     * @param treeId The id of tree configuration
176     * @param value the value to match
177     * @return the matching paths composed by contents id separated by ';'
178     */
179    @Callable
180    public List<String> filterChildrenContentByRegExp(String parentContentId, String treeId, String value)
181    {
182        List<String> matchingPaths = new ArrayList<>();
183
184        Content parentContent = _ametysResolver.resolveById(parentContentId);
185        TreeConfiguration treeConfiguration = _treeExtensionPoint.getExtension(treeId);
186        
187        String toMatch = StringUtils.stripAccents(value.toLowerCase()).trim();
188        
189        Map<String, List<Content>> childrenContentByAttributes = getChildrenContent(parentContent, treeConfiguration);
190        for (List<Content> childrenContent : childrenContentByAttributes.values())
191        {
192            for (Content childContent : childrenContent)
193            {
194                _getMatchingContents(childContent, toMatch, treeConfiguration, matchingPaths, parentContentId);
195            }
196        }
197        
198        return matchingPaths;
199    }
200    
201    private void _getMatchingContents(Content content, String value, TreeConfiguration treeConfiguration, List<String> matchingPaths, String parentPath)
202    {
203        if (isContentMatching(content, value))
204        {
205            matchingPaths.add(parentPath + ";" + content.getId());
206        }
207        
208        Map<String, List<Content>> childrenContentByAttributes = getChildrenContent(content, treeConfiguration);
209        for (List<Content> childrenContent : childrenContentByAttributes.values())
210        {
211            for (Content childContent : childrenContent)
212            {
213                _getMatchingContents(childContent, value, treeConfiguration, matchingPaths, parentPath + ";" + content.getId());
214            }
215        }
216    }
217    
218    /**
219     * Determines if content matches the filter regexp
220     * @param content the content
221     * @param value the value to match
222     * @return true if the content match
223     */
224    protected boolean isContentMatching(Content content, String value)
225    {
226        String title =  StringUtils.stripAccents(content.getTitle().toLowerCase());
227        return title.contains(value);
228    }
229    
230    /**
231     * Get the root node informations
232     * @param contentId The content
233     * @return The informations
234     */
235    @Callable
236    public Map<String, Object> getRootNodeInformations(String contentId)
237    {
238        return getNodeInformations(contentId);
239    }
240    
241    /**
242     * Get the node informations
243     * @param contentId The content
244     * @return The informations
245     */
246    @Callable
247    public Map<String, Object> getNodeInformations(String contentId)
248    {
249        Content content = _ametysResolver.resolveById(contentId);
250        return content2Json(content);
251    }
252    
253    /**
254     * Get the default JSON representation of a content of the tree
255     * @param content the content
256     * @return the content as JSON
257     */
258    protected Map<String, Object> content2Json(Content content)
259    {
260        Map<String, Object> infos = new HashMap<>();
261        
262        infos.put("contentId", content.getId());
263        infos.put("contenttypesIds", content.getTypes());
264        infos.put("name", content.getName());
265        infos.put("title", content.getTitle());
266
267        infos.put("iconGlyph", _contentTypesHelper.getIconGlyph(content));
268        infos.put("iconDecorator", _contentTypesHelper.getIconDecorator(content));
269        infos.put("iconSmall", _contentTypesHelper.getSmallIcon(content));
270        infos.put("iconMedium", _contentTypesHelper.getMediumIcon(content));
271        infos.put("iconLarge", _contentTypesHelper.getLargeIcon(content));
272        
273        return infos;
274    }
275    
276    /**
277     * Get the default JSON representation of a child content
278     * @param content the content
279     * @param attributePath the path of attribute holding this content
280     * @return the content as JSON
281     */
282    public Map<String, Object> childContent2Json(Content content, String attributePath)
283    {
284        Map<String, Object> childInfo = content2Json(content);
285        childInfo.put("metadataPath", attributePath);
286        return childInfo;
287    }
288    
289    private void _merge(Map<String, List<Content>> childrenContent, Map<String, List<Content>> contents)
290    {
291        for (String key : contents.keySet())
292        {
293            if (!childrenContent.containsKey(key))
294            {
295                childrenContent.put(key, new ArrayList<Content>());
296            }
297            
298            List<Content> contentsList = childrenContent.get(key);
299            contentsList.addAll(contents.get(key));
300        }
301    }
302
303    private Map<String, List<Content>> _handleAttributeTreeConfigurationElementsChild(ContentType contentType, ModelAwareDataHolder dataHolder, AttributeTreeConfigurationElementsChild attributeTreeConfigurationElementsChild, TreeConfiguration treeConfiguration)
304    {
305        Map<String, List<Content>> childrenContent = new HashMap<>();
306        
307        String attributePath = attributeTreeConfigurationElementsChild.getPath();
308
309        try
310        {
311            Map<String, List<Content>> contents = _handleAttribute(contentType, dataHolder, attributePath);
312            _merge(childrenContent, contents);
313        }
314        catch (Exception e)
315        {
316            throw new IllegalArgumentException("An error occured on the tree configuration '" + treeConfiguration.getId() + "' getting for metadata '" + attributePath + "' on content type '" + contentType.getId() + "'", e);  
317        }
318
319        return childrenContent;
320    }
321
322    private Map<String, List<Content>> _handleAttribute(ModelItemContainer modelItemContainer, ModelAwareDataHolder dataHolder, String attributePath)
323    {
324        Map<String, List<Content>> childrenContent = new HashMap<>();
325
326        String currentModelItemName = StringUtils.substringBefore(attributePath, ModelItem.ITEM_PATH_SEPARATOR);
327        
328        ModelItem currentModelItem = modelItemContainer.getChild(currentModelItemName);
329        if (currentModelItem == null)
330        {
331            throw new IllegalArgumentException("No attribute definition for " + currentModelItemName);
332        }
333        
334        
335        if (dataHolder.hasValue(currentModelItemName))
336        {
337            if (currentModelItem instanceof RepeaterDefinition)
338            {
339                ModelAwareRepeater repeater = dataHolder.getRepeater(currentModelItemName);
340                for (ModelAwareRepeaterEntry entry : repeater.getEntries())
341                {
342                    String subMetadataId = StringUtils.substringAfter(attributePath, ModelItem.ITEM_PATH_SEPARATOR);
343                    Map<String, List<Content>> contents = _handleAttribute((RepeaterDefinition) currentModelItem, entry, subMetadataId);
344
345                    _merge(childrenContent, contents);
346                }
347            }
348            else if (currentModelItem instanceof CompositeDefinition)
349            {
350                ModelAwareComposite metadata = dataHolder.getComposite(currentModelItemName);
351                
352                String subMetadataId = StringUtils.substringAfter(attributePath, ModelItem.ITEM_PATH_SEPARATOR);
353                Map<String, List<Content>> contents = _handleAttribute((CompositeDefinition) currentModelItem, metadata, subMetadataId);
354
355                _merge(childrenContent, contents);
356            }
357            else if (ModelItemTypeConstants.CONTENT_ELEMENT_TYPE_ID.equals(currentModelItem.getType().getId()))
358            {
359                ContentValue[] contentValues = dataHolder.getValue(currentModelItemName);
360                for (ContentValue contentValue : contentValues)
361                {
362                    String key = currentModelItem.getPath();
363                    Optional<ModifiableContent> optContent = contentValue.getContentIfExists();
364                    if (optContent.isPresent())
365                    {
366                        childrenContent.computeIfAbsent(key, k -> new ArrayList<>()).add(optContent.get());
367                    }
368                    else
369                    {
370                        getLogger().warn("On a data holder, the attribute '{}' is referencing a unexisting content: '{}'", key, contentValue.getContentId());
371                    }
372                }
373            }
374            else 
375            {
376                throw new IllegalArgumentException("The metadata definition for " + currentModelItem.getPath() + " is not a content");
377            }
378        }
379        
380        return childrenContent;
381    }
382    
383    /**
384     * Get the tree configuration
385     * @param treeId the tree id
386     * @return the tree configuration
387     */
388    protected TreeConfiguration _getTreeConfiguration(String treeId)
389    {
390        if (StringUtils.isBlank(treeId))
391        {
392            throw new IllegalArgumentException("The tree information cannot be obtain, because 'tree' is null");
393        }
394
395        TreeConfiguration treeConfiguration = _treeExtensionPoint.getExtension(treeId);
396        if (treeConfiguration == null)
397        {
398            throw new IllegalArgumentException("There is no tree configuration for '" + treeId + "'");
399        }
400        return treeConfiguration;
401    }
402
403    /**
404     * Get the parent content of a tree
405     * @param parentId the parent id
406     * @return the parent content of a tree
407     * @throws IllegalArgumentException if an exception occurred
408     */
409    protected Content _getParentContent(String parentId) throws IllegalArgumentException
410    {
411        if (StringUtils.isBlank(parentId))
412        {
413            throw new IllegalArgumentException("The tree information cannot be obtain, because 'node' is null"); 
414        }
415        
416        try
417        {
418            return _ametysResolver.resolveById(parentId);
419        }
420        catch (Exception e)
421        {
422            throw new IllegalArgumentException("The tree configuration cannot be used on an object that is not a content: " + parentId, e);
423        }
424
425    }
426    
427}