001/* 002 * Copyright 2019 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.plugins.core.search; 017 018import java.util.ArrayList; 019import java.util.Collections; 020import java.util.Comparator; 021import java.util.HashMap; 022import java.util.List; 023import java.util.Map; 024import java.util.Objects; 025import java.util.Optional; 026import java.util.Set; 027import java.util.function.Function; 028import java.util.stream.Collectors; 029import java.util.stream.Stream; 030 031import org.apache.avalon.framework.component.Component; 032import org.apache.avalon.framework.logger.AbstractLogEnabled; 033import org.apache.avalon.framework.service.ServiceException; 034import org.apache.avalon.framework.service.ServiceManager; 035import org.apache.avalon.framework.service.Serviceable; 036 037import org.ametys.core.group.Group; 038import org.ametys.core.group.GroupDirectoryContextHelper; 039import org.ametys.core.group.GroupDirectoryDAO; 040import org.ametys.core.group.directory.GroupDirectory; 041import org.ametys.core.user.User; 042import org.ametys.core.user.UserManager; 043import org.ametys.core.user.population.PopulationContextHelper; 044import org.ametys.core.user.population.UserPopulation; 045import org.ametys.core.user.population.UserPopulationDAO; 046import org.ametys.plugins.core.group.GroupHelper; 047import org.ametys.plugins.core.user.UserHelper; 048 049/** 050 * Common helper for user and group searches 051 */ 052public class UserAndGroupSearchManager extends AbstractLogEnabled implements Component, Serviceable 053{ 054 /** The avalon role */ 055 public static final String ROLE = UserAndGroupSearchManager.class.getName(); 056 057 private static final int _DEFAULT_OFFSET_VALUE = 0; 058 private static final int _DEFAULT_COUNT_VALUE = 20; 059 060 private UserManager _userManager; 061 private UserHelper _userHelper; 062 private GroupDirectoryDAO _groupDirectoryDAO; 063 private GroupDirectoryContextHelper _directoryContextHelper; 064 private PopulationContextHelper _populationContextHelper; 065 private UserPopulationDAO _userPopulationDAO; 066 private GroupHelper _groupHelper; 067 068 public void service(ServiceManager manager) throws ServiceException 069 { 070 _userManager = (UserManager) manager.lookup(UserManager.ROLE); 071 _userHelper = (UserHelper) manager.lookup(UserHelper.ROLE); 072 _userPopulationDAO = (UserPopulationDAO) manager.lookup(UserPopulationDAO.ROLE); 073 _groupDirectoryDAO = (GroupDirectoryDAO) manager.lookup(GroupDirectoryDAO.ROLE); 074 _directoryContextHelper = (GroupDirectoryContextHelper) manager.lookup(GroupDirectoryContextHelper.ROLE); 075 _populationContextHelper = (PopulationContextHelper) manager.lookup(PopulationContextHelper.ROLE); 076 _groupHelper = (GroupHelper) manager.lookup(GroupHelper.ROLE); 077 } 078 079 /** 080 * Mixed users and groups sorted search, with multiple offsets 081 * @param contexts The context of the search 082 * @param limit The maximum number of results per group directory and population 083 * @param searchData The search data to fetch the next batch of the search. Must be null on the first search. 084 * @param params The parameters to pass to the group directories and user populations when retrieving their contents 085 * @return An object with the list of results, sorted, and the search data required to fetch the following batch 086 */ 087 public Map<String, Object> searchUsersAndGroupByContext(Set<String> contexts, int limit, Map<String, Object> searchData, Map<String, Object> params) 088 { 089 Set<String> groupDirectories = _directoryContextHelper.getGroupDirectoriesOnContexts(contexts); 090 Set<String> userPopulations = _populationContextHelper.getUserPopulationsOnContexts(contexts, false, true); 091 return searchUsersAndGroup(userPopulations, groupDirectories, limit, searchData, params); 092 } 093 094 /** 095 * Mixed users and groups sorted search, with multiple offsets 096 * @param availablePopulations The populations of users available for search. On first search, all populations will be used 097 * @param availableDirectories The directories of groups available for search. On first search, all directories will be used 098 * @param limit The maximum number of results per group directory and population 099 * @param searchData The search data to fetch the next batch of the search. Must be null on the first search. 100 * @param params The parameters to pass to the group directories and user populations when retrieving their contents 101 * @return An object with the list of results, sorted, and the search data required to fetch the following batch 102 */ 103 public Map<String, Object> searchUsersAndGroup(Set<String> availablePopulations, Set<String> availableDirectories, int limit, Map<String, Object> searchData, Map<String, Object> params) 104 { 105 Map<String, Integer> usersFromSearchData = _getFromSearchData("users", searchData); 106 Set<String> userPopulations = usersFromSearchData == null ? availablePopulations : _filterSetUnion(availablePopulations, usersFromSearchData.keySet()); 107 Map<String, Integer> groupsFromSearchData = _getFromSearchData("groups", searchData); 108 Set<String> groupDirectories = groupsFromSearchData == null ? availableDirectories : _filterSetUnion(availableDirectories, groupsFromSearchData.keySet()); 109 110 Function<UserPopulation, Integer> getPopulationOffset = population -> usersFromSearchData != null 111 ? usersFromSearchData.getOrDefault(population.getId(), _DEFAULT_OFFSET_VALUE) 112 : _DEFAULT_OFFSET_VALUE; 113 Function<GroupDirectory, Integer> getDirectoryOffset = directory -> groupsFromSearchData != null 114 ? groupsFromSearchData.getOrDefault(directory.getId(), _DEFAULT_OFFSET_VALUE) 115 : _DEFAULT_OFFSET_VALUE; 116 117 int count = limit <= 0 ? _DEFAULT_COUNT_VALUE : limit; 118 119 List<SearchDataResult> searchDataResults = Stream.concat( 120 userPopulations.stream() 121 .map(_userPopulationDAO::getUserPopulation) 122 .filter(Objects::nonNull) 123 .map(population -> _searchUsers(population, count, getPopulationOffset.apply(population), params)), 124 groupDirectories.stream() 125 .map(_groupDirectoryDAO::getGroupDirectory) 126 .filter(Objects::nonNull) 127 .map(directory -> _searchGroups(directory, count, getDirectoryOffset.apply(directory), params))) 128 .collect(Collectors.toList()); 129 130 if (searchDataResults.size() == 0) 131 { 132 return _formatSearchResults(Collections.emptyList(), null); 133 } 134 135 if (searchDataResults.size() == 1) 136 { 137 SearchDataResult searchDataResult = searchDataResults.get(0); 138 return _formatSearchResults(searchDataResult.results2Json(), searchDataResult.isFinished() ? null : searchDataResults); 139 } 140 141 if (searchDataResults.stream().allMatch(SearchDataResult::isFinished)) 142 { 143 return _formatSearchResults(_mergeSearchResults(searchDataResults), null); 144 } 145 146 searchDataResults = searchDataResults.stream() 147 .filter(searchDataResult -> searchDataResult.getLastSortableName() != null) 148 .sorted(Comparator.comparing(SearchDataResult::getLastSortableName)) 149 .collect(Collectors.toList()); 150 151 String entryDelimitor = null; 152 for (SearchDataResult searchDataResult : searchDataResults) 153 { 154 if (entryDelimitor != null) 155 { 156 searchDataResult.truncateAfter(entryDelimitor); 157 } 158 else if (!searchDataResult.isFinished()) 159 { 160 entryDelimitor = searchDataResult.getLastSortableName(); 161 } 162 } 163 164 List<SearchDataResult> unfinishedSearchDataResults = searchDataResults.stream() 165 .filter(searchDataResult -> !searchDataResult.isFinished()) 166 .collect(Collectors.toList()); 167 168 return _formatSearchResults(_mergeSearchResults(searchDataResults), unfinishedSearchDataResults); 169 } 170 171 private Map<String, Integer> _getFromSearchData(String property, Map<String, Object> searchData) 172 { 173 if (searchData == null) 174 { 175 return null; 176 } 177 178 return Optional.ofNullable(searchData.getOrDefault(property, null)) 179 .filter(Map.class::isInstance) 180 .map(Map.class::cast) 181 .orElse(Collections.emptyMap()); 182 } 183 184 private Set<String> _filterSetUnion(Set<String> values, Set<String> referenceSet) 185 { 186 return values.stream() 187 .filter(referenceSet::contains) 188 .collect(Collectors.toSet()); 189 } 190 191 private SearchDataResult _searchUsers(UserPopulation population, int limit, int offset, Map<String, Object> params) 192 { 193 List<User> users = new ArrayList<>(_userManager.getUsers(population, limit, offset, params)); 194 195 return new PopulationSearchDataResult(_userHelper, population.getId(), users.size() < limit, users, offset); 196 } 197 198 private SearchDataResult _searchGroups(GroupDirectory directory, int limit, int offset, Map<String, Object> params) 199 { 200 List<Group> groups = directory.getGroups(limit, offset, params); 201 202 return new DirectorySearchDataResult(_groupHelper, directory.getId(), groups.size() < limit, groups, offset); 203 } 204 205 private Map<String, Object> _formatSearchResults(List<Map<String, Object>> results, List<SearchDataResult> searchDataResults) 206 { 207 Map<String, Object> jsonResult = new HashMap<>(); 208 jsonResult.put("results", results); 209 if (searchDataResults == null) 210 { 211 jsonResult.put("finished", true); 212 } 213 else 214 { 215 Map<String, Map<String, Integer>> searchDataJson = new HashMap<>(); 216 for (SearchDataResult searchDataResult: searchDataResults) 217 { 218 String type = searchDataResult.getType(); 219 if (!searchDataJson.containsKey(type)) 220 { 221 searchDataJson.put(type, new HashMap<>()); 222 } 223 searchDataJson.get(type).put(searchDataResult.getSourceId(), searchDataResult.getOffset()); 224 } 225 226 if (searchDataJson.size() > 0) 227 { 228 jsonResult.put("searchData", searchDataJson); 229 } 230 else 231 { 232 jsonResult.put("finished", true); 233 } 234 } 235 236 return jsonResult; 237 } 238 239 private List<Map<String, Object>> _mergeSearchResults(List<SearchDataResult> searchDataResults) 240 { 241 return searchDataResults.stream() 242 .map(SearchDataResult::results2Json) 243 .flatMap(List::stream) 244 .sorted((o1, o2) -> _getSortableName(o1).compareTo(_getSortableName(o2))) 245 .collect(Collectors.toList()); 246 } 247 248 /** 249 * Get the sortable name for the json object 250 * @param o The object 251 * @return The sortable name, or an empty string if no name was found 252 */ 253 protected static String _getSortableName(Map<String, Object> o) 254 { 255 if (o.containsKey("sortablename")) 256 { 257 return ((String) o.get("sortablename")).toLowerCase(); 258 } 259 260 if (o.containsKey("label")) 261 { 262 return ((String) o.get("label")).toLowerCase(); 263 } 264 265 return ""; 266 } 267 268 private static interface SearchDataResult 269 { 270 public String getType(); 271 public String getSourceId(); 272 public boolean isFinished(); 273 public void truncateAfter(String entry); 274 public int getOffset(); 275 public List<Map<String, Object>> results2Json(); 276 public String getLastSortableName(); 277 } 278 279 private static class PopulationSearchDataResult implements SearchDataResult 280 { 281 private boolean _isFinished; 282 private List<User> _users; 283 private int _offset; 284 private UserHelper _helper; 285 private String _sourceId; 286 287 public PopulationSearchDataResult(UserHelper userHelper, String sourceId, boolean isFinished, List<User> users, int initialOffset) 288 { 289 _helper = userHelper; 290 _sourceId = sourceId; 291 _isFinished = isFinished; 292 _users = users; 293 _offset = initialOffset; 294 } 295 296 public String getType() 297 { 298 return "users"; 299 } 300 301 public String getSourceId() 302 { 303 return _sourceId; 304 } 305 306 public boolean isFinished() 307 { 308 return _isFinished; 309 } 310 311 public void truncateAfter(String entry) 312 { 313 if (_users.removeIf(user -> user.getSortableName().toLowerCase().compareTo(entry) > 0)) 314 { 315 _isFinished = false; 316 } 317 } 318 319 public int getOffset() 320 { 321 return _offset + _users.size(); 322 } 323 324 public List<Map<String, Object>> results2Json() 325 { 326 return _helper.users2json(_users, true); 327 } 328 329 public String getLastSortableName() 330 { 331 return _users.size() > 0 ? _users.get(_users.size() - 1).getSortableName().toLowerCase() : null; 332 } 333 334 @Override 335 public String toString() 336 { 337 return SearchDataResult.class.getName() + "@" + Integer.toHexString(hashCode()) 338 + "[" + _users.size() + " user(s) from population " + _sourceId + " at offset " + _offset + "]" 339 + (_isFinished ? "[Finished]" : ""); 340 } 341 } 342 343 private static class DirectorySearchDataResult implements SearchDataResult 344 { 345 private String _sourceId; 346 private boolean _isFinished; 347 private List<Group> _groups; 348 private int _offset; 349 private GroupHelper _helper; 350 351 public DirectorySearchDataResult(GroupHelper helper, String sourceId, boolean isFinished, List<Group> groups, int initialOffset) 352 { 353 _helper = helper; 354 _sourceId = sourceId; 355 _isFinished = isFinished; 356 _groups = groups; 357 _offset = initialOffset; 358 } 359 360 public String getType() 361 { 362 return "groups"; 363 } 364 365 public String getSourceId() 366 { 367 return _sourceId; 368 } 369 370 public boolean isFinished() 371 { 372 return _isFinished; 373 } 374 375 public void truncateAfter(String entry) 376 { 377 if (_groups.removeIf(group -> group.getLabel().toLowerCase().compareTo(entry) > 0)) 378 { 379 _isFinished = false; 380 } 381 } 382 383 public int getOffset() 384 { 385 return _offset + _groups.size(); 386 } 387 388 public List<Map<String, Object>> results2Json() 389 { 390 return _helper.groups2JSON(_groups, false); 391 } 392 393 public String getLastSortableName() 394 { 395 return _groups.size() > 0 ? _groups.get(_groups.size() - 1).getLabel().toLowerCase() : null; 396 } 397 398 @Override 399 public String toString() 400 { 401 return SearchDataResult.class.getName() + "@" + Integer.toHexString(hashCode()) 402 + "[" + _groups.size() + " group(s) from directory " + _sourceId + " at offset " + _offset + "]" 403 + (_isFinished ? "[Finished]" : ""); 404 } 405 406 } 407}