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