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