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}