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}