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            for (Content content : contents)
136            {
137                parent.addContent(content);
138            }
139        }
140    }
141    
142    /**
143     * Group contents by given grouping field
144     * @param contents list of content
145     * @param fieldId The id of grouping field. Can be the path of an attribute or the system property
146     * @return a Map where the key will be the value of the attribute
147     */
148    private Map<Object, List<Content>> _groupBy(Iterable<Content> contents, String fieldId)
149    {
150        Map<Object, List<Content>> groups = new HashMap<>();
151        for (Content content : contents)
152        {
153            Object value = _contentHelper.getValue(content, fieldId);
154            if (value instanceof List)
155            {
156                // If the value is a list, the content is added into different groups, depending on the values in the list
157                List valueList = (List) value;
158                for (Object subValue : valueList)
159                {
160                    groups.computeIfAbsent(subValue, __ -> new ArrayList<>())
161                          .add(content);
162                }
163            }
164            else
165            {
166                groups.computeIfAbsent(value, __ -> new ArrayList<>())
167                      .add(content);
168            }
169        }
170        return groups;
171    }
172    
173    private String _getGroupValueAsString(String fieldId, Object value, Set<ContentType> contentTypes, Locale locale)
174    {
175        if (value == null)
176        {
177            return StringUtils.EMPTY;
178        }
179        else if (value instanceof ContentValue)
180        {
181            return ((ContentValue) value).getContentIfExists()
182                                         .map(content -> content.getTitle(locale))
183                                         .orElse(StringUtils.EMPTY);
184        }
185        else if (value instanceof UserIdentity)
186        {
187            User user = _userManager.getUser((UserIdentity) value);
188            return user != null ? user.getFullName() : UserIdentity.userIdentityToString((UserIdentity) value);
189        }
190        else if (value instanceof Boolean)
191        {
192            String language = Optional.ofNullable(locale)
193                    .map(Locale::getLanguage)
194                    .orElse(null);
195            StringBuilder sb = new StringBuilder();
196            if (ModelHelper.hasModelItem(fieldId, contentTypes))
197            {
198                ModelItem modelItem = ModelHelper.getModelItem(fieldId, contentTypes);
199                sb.append(_i18nUtils.translate(modelItem.getLabel(), language));
200                sb.append(_i18nUtils.translate(new I18nizableText("plugin.cms", "PLUGINS_CMS_SEARCH_EXPORT_SEPARATOR"), language));
201            }
202            sb.append(_i18nUtils.translate(new I18nizableText("plugin.cms", "PLUGINS_CMS_SEARCH_EXPORT_BOOLEAN_" + ((Boolean) value).toString().toUpperCase()), language));
203            return sb.toString();
204        }
205        else if (value instanceof MultilingualString)
206        {
207            return MultilingualStringHelper.getValue((MultilingualString) value, locale);
208        }
209        else if (!_systemPropertyExtensionPoint.hasExtension(fieldId))
210        {
211            ModelItem modelItem = ModelHelper.getModelItem(fieldId, contentTypes);
212            if (modelItem instanceof ElementDefinition)
213            {
214                ElementDefinition definition = (ElementDefinition) modelItem;
215                if (definition.getEnumerator() != null)
216                {
217                    try
218                    {
219                        I18nizableText label = definition.getEnumerator().getEntry(value);
220                        return Optional.ofNullable(label)
221                                       .map(_i18nUtils::translate)
222                                       .orElse(StringUtils.EMPTY);
223                    }
224                    catch (Exception e)
225                    {
226                        // An unexpected error occurs while getting the enumerator entry
227                        return definition.getType().toString(value);
228                    }
229                }
230                else
231                {
232                    return definition.getType().toString(value);
233                }
234            }
235            else
236            {
237                throw new IllegalArgumentException("Attribute at path '" + modelItem.getPath() + "' is a composite or a repeater : can not group values");
238            }
239        }
240        else if (value instanceof Date)
241        {
242            return DateUtils.dateToString((Date) value);
243        }
244        else
245        {
246            return value.toString();
247        }
248
249    }
250}