001/*
002 *  Copyright 2017 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.cms.search;
017
018import java.util.ArrayList;
019import java.util.Date;
020import java.util.HashMap;
021import java.util.List;
022import java.util.Locale;
023import java.util.Map;
024import java.util.Optional;
025import java.util.Set;
026
027import org.apache.avalon.framework.component.Component;
028import org.apache.avalon.framework.service.ServiceException;
029import org.apache.avalon.framework.service.ServiceManager;
030import org.apache.avalon.framework.service.Serviceable;
031import org.apache.commons.lang3.StringUtils;
032
033import org.ametys.cms.content.ContentHelper;
034import org.ametys.cms.contenttype.ContentType;
035import org.ametys.cms.data.ContentValue;
036import org.ametys.cms.repository.Content;
037import org.ametys.cms.search.cocoon.GroupSearchContent;
038import org.ametys.cms.search.model.SystemPropertyExtensionPoint;
039import org.ametys.core.user.User;
040import org.ametys.core.user.UserIdentity;
041import org.ametys.core.user.UserManager;
042import org.ametys.core.util.DateUtils;
043import org.ametys.core.util.I18nUtils;
044import org.ametys.plugins.repository.metadata.MultilingualString;
045import org.ametys.plugins.repository.metadata.MultilingualStringHelper;
046import org.ametys.runtime.i18n.I18nizableText;
047import org.ametys.runtime.model.ElementDefinition;
048import org.ametys.runtime.model.ModelHelper;
049import org.ametys.runtime.model.ModelItem;
050
051/**
052 * Helper to group search contents
053 */
054public class GroupSearchContentHelper implements Component, Serviceable
055{
056    /** The Avalon role name */
057    public static final String ROLE = GroupSearchContentHelper.class.getName();
058    
059    private SystemPropertyExtensionPoint _systemPropertyExtensionPoint;
060    private ContentHelper _contentHelper;
061    private UserManager _userManager;
062    private I18nUtils _i18nUtils;
063
064    public void service(ServiceManager serviceManager) throws ServiceException
065    {
066        _systemPropertyExtensionPoint = (SystemPropertyExtensionPoint) serviceManager.lookup(SystemPropertyExtensionPoint.ROLE);
067        _contentHelper = (ContentHelper) serviceManager.lookup(ContentHelper.ROLE);
068        _userManager = (UserManager) serviceManager.lookup(UserManager.ROLE);
069        _i18nUtils = (I18nUtils) serviceManager.lookup(I18nUtils.ROLE);
070    }
071    
072    /**
073     * Create GroupSearchContent to organize the contents in a hierarchy according to a list of attributes
074     * Example Continent/Country/City
075     * @param contents contents that need to be sorted
076     * @param groupingFields list of attributes to group with (ordered)
077     * @param contentTypes the content type
078     * @param locale The locale for groups. Can be null. Only useful if grouping by multilingual contents or multilingual string
079     * @return the root group of contents
080     */
081    public GroupSearchContent organizeContentsInGroups(Iterable<Content> contents, List<String> groupingFields, Set<ContentType> contentTypes, Locale locale)
082    {
083        GroupSearchContent groupSearchContent = new GroupSearchContent();
084        _organizeContents(groupSearchContent, contents, groupingFields, contentTypes, locale);
085        return groupSearchContent;
086    }
087    
088    /**
089     * Recursive function that create sub GroupSearchContent to organize the contents in a hierarchy according to a list of attributes
090     * Example Continent/Country/City
091     * @param parent parent group in which contents will be added, or in which new groups will be created
092     * @param contents contents that need to be sorted
093     * @param groupingFields list of attributes to group with (ordered)
094     * @param contentTypes the content types
095     * @param locale The local to get i18n titles
096     */
097    private void _organizeContents(GroupSearchContent parent, Iterable<Content> contents, List<String> groupingFields, Set<ContentType> contentTypes, Locale locale)
098    {
099        if (groupingFields != null && groupingFields.size() > 0)
100        {
101            String fieldId = groupingFields.get(0);
102            // Group contents by attribute value
103            Map<Object, List<Content>> groups = _groupBy(contents, fieldId);
104
105            // Get the rest of the attribute regroupment list
106            List<String> subAttributeRegroupments = new ArrayList<>();
107            if (groupingFields.size() > 1)
108            {
109                subAttributeRegroupments.addAll(groupingFields);
110                subAttributeRegroupments.remove(0);
111            }
112            // Then : add sorted contents into groups, or re-organize if there are still some attributeRegroupments
113            for (Map.Entry<Object, List<Content>> entry : groups.entrySet())
114            {
115                String keyStr = _getGroupValueAsString(fieldId, entry.getKey(), contentTypes, locale);
116                
117                GroupSearchContent groupSearchContent = new GroupSearchContent(fieldId, keyStr);
118                if (groupingFields.size() == 1)
119                {
120                    // If no more groups-level, add all contents in the group
121                    groupSearchContent.addContents(entry.getValue());
122                }
123                else
124                {
125                    // If more groups to do : let's do it !
126                    _organizeContents(groupSearchContent, entry.getValue(), subAttributeRegroupments, contentTypes, locale);
127                }
128                // Add current group into parent
129                parent.addToSubList(groupSearchContent);
130            }
131        }
132        else
133        {
134            // If there are no group to do, just add all contents into the parent
135            contents.forEach(parent::addContent);
136        }
137    }
138    
139    /**
140     * Group contents by given grouping field
141     * @param contents list of content
142     * @param fieldId The id of grouping field. Can be the path of an attribute or the system property
143     * @return a Map where the key will be the value of the attribute
144     */
145    private Map<Object, List<Content>> _groupBy(Iterable<Content> contents, String fieldId)
146    {
147        Map<Object, List<Content>> groups = new HashMap<>();
148        for (Content content : contents)
149        {
150            Object value = _contentHelper.getValue(content, fieldId);
151            if (value instanceof List)
152            {
153                // If the value is a list, the content is added into different groups, depending on the values in the list
154                List valueList = (List) value;
155                for (Object subValue : valueList)
156                {
157                    groups.computeIfAbsent(subValue, __ -> new ArrayList<>())
158                          .add(content);
159                }
160            }
161            else
162            {
163                groups.computeIfAbsent(value, __ -> new ArrayList<>())
164                      .add(content);
165            }
166        }
167        return groups;
168    }
169    
170    private String _getGroupValueAsString(String fieldId, Object value, Set<ContentType> contentTypes, Locale locale)
171    {
172        if (value == null)
173        {
174            return StringUtils.EMPTY;
175        }
176        else if (value instanceof ContentValue)
177        {
178            return ((ContentValue) value).getContentIfExists()
179                                         .map(content -> content.getTitle(locale))
180                                         .orElse(StringUtils.EMPTY);
181        }
182        else if (value instanceof UserIdentity)
183        {
184            User user = _userManager.getUser((UserIdentity) value);
185            return user != null ? user.getFullName() : UserIdentity.userIdentityToString((UserIdentity) value);
186        }
187        else if (value instanceof Boolean)
188        {
189            String language = Optional.ofNullable(locale)
190                    .map(Locale::getLanguage)
191                    .orElse(null);
192            StringBuilder sb = new StringBuilder();
193            if (ModelHelper.hasModelItem(fieldId, contentTypes))
194            {
195                ModelItem modelItem = ModelHelper.getModelItem(fieldId, contentTypes);
196                sb.append(_i18nUtils.translate(modelItem.getLabel(), language));
197                sb.append(_i18nUtils.translate(new I18nizableText("plugin.cms", "PLUGINS_CMS_SEARCH_EXPORT_SEPARATOR"), language));
198            }
199            sb.append(_i18nUtils.translate(new I18nizableText("plugin.cms", "PLUGINS_CMS_SEARCH_EXPORT_BOOLEAN_" + ((Boolean) value).toString().toUpperCase()), language));
200            return sb.toString();
201        }
202        else if (value instanceof MultilingualString)
203        {
204            return MultilingualStringHelper.getValue((MultilingualString) value, locale);
205        }
206        else if (!_systemPropertyExtensionPoint.hasExtension(fieldId))
207        {
208            ModelItem modelItem = ModelHelper.getModelItem(fieldId, contentTypes);
209            if (modelItem instanceof ElementDefinition)
210            {
211                ElementDefinition definition = (ElementDefinition) modelItem;
212                if (definition.getEnumerator() != null)
213                {
214                    try
215                    {
216                        I18nizableText label = definition.getEnumerator().getEntry(value);
217                        return Optional.ofNullable(label)
218                                       .map(_i18nUtils::translate)
219                                       .orElse(StringUtils.EMPTY);
220                    }
221                    catch (Exception e)
222                    {
223                        // An unexpected error occurs while getting the enumerator entry
224                        return definition.getType().toString(value);
225                    }
226                }
227                else
228                {
229                    return definition.getType().toString(value);
230                }
231            }
232            else
233            {
234                throw new IllegalArgumentException("Attribute at path '" + modelItem.getPath() + "' is a composite or a repeater : can not group values");
235            }
236        }
237        else if (value instanceof Date)
238        {
239            return DateUtils.dateToString((Date) value);
240        }
241        else
242        {
243            return value.toString();
244        }
245
246    }
247}