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