001/* 002 * Copyright 2016 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.userdirectory.synchronize; 017 018import java.util.ArrayList; 019import java.util.Arrays; 020import java.util.Collections; 021import java.util.HashMap; 022import java.util.HashSet; 023import java.util.LinkedHashMap; 024import java.util.List; 025import java.util.Map; 026import java.util.Set; 027import java.util.stream.Collectors; 028 029import org.apache.avalon.framework.configuration.Configuration; 030import org.apache.avalon.framework.configuration.ConfigurationException; 031import org.apache.avalon.framework.service.ServiceException; 032import org.apache.avalon.framework.service.ServiceManager; 033import org.apache.commons.collections4.MapUtils; 034import org.apache.commons.lang.StringUtils; 035import org.slf4j.Logger; 036import org.slf4j.LoggerFactory; 037 038import org.ametys.cms.contenttype.ContentType; 039import org.ametys.cms.contenttype.MetadataDefinition; 040import org.ametys.cms.repository.Content; 041import org.ametys.cms.repository.ModifiableDefaultContent; 042import org.ametys.core.user.directory.UserDirectory; 043import org.ametys.core.user.population.UserPopulation; 044import org.ametys.core.user.population.UserPopulationDAO; 045import org.ametys.core.util.JSONUtils; 046import org.ametys.plugins.contentio.synchronize.AbstractSimpleSynchronizableContentsCollection; 047import org.ametys.plugins.contentio.synchronize.SynchronizableContentsCollection; 048import org.ametys.plugins.contentio.synchronize.impl.LDAPCollectionHelper; 049import org.ametys.plugins.core.impl.user.directory.JdbcUserDirectory; 050import org.ametys.plugins.core.impl.user.directory.LdapUserDirectory; 051import org.ametys.plugins.repository.AmetysObjectIterable; 052import org.ametys.plugins.userdirectory.DeleteUserComponent; 053import org.ametys.runtime.i18n.I18nizableText; 054 055/** 056 * Implementation of {@link SynchronizableContentsCollection} to be synchronized with a {@link UserPopulation} of the CMS. 057 */ 058public class UserPopulationSynchronizableContentsCollection extends AbstractSimpleSynchronizableContentsCollection 059{ 060 /** Name of parameter holding the id of population */ 061 protected static final String __PARAM_POPULATION_ID = "populationId"; 062 /** Name of parameter for the login metadata */ 063 protected static final String __PARAM_LOGIN_METADATA_NAME = "login"; 064 /** Name of parameter for the firstname metadata */ 065 protected static final String __PARAM_FIRSTNAME_METADATA_NAME = "firstname"; 066 /** Name of parameter for the lastname metadata */ 067 protected static final String __PARAM_LASTNAME_METADATA_NAME = "lastname"; 068 /** Name of parameter for the email metadata */ 069 protected static final String __PARAM_EMAIL_METADATA_NAME = "email"; 070 /** Name of parameter holding the fields mapping */ 071 protected static final String __PARAM_MAPPING = "mapping"; 072 /** Name of parameter holding the additional search filter */ 073 protected static final String __PARAM_ADDITIONAL_SEARCH_FILTER = "additionalSearchFilter"; 074 /** Name of parameter into mapping holding the synchronized property */ 075 protected static final String __PARAM_MAPPING_SYNCHRO = "synchro"; 076 /** Name of parameter into mapping holding the path of metadata */ 077 protected static final String __PARAM_MAPPING_METADATA_REF = "metadata-ref"; 078 /** Name of parameter into mapping holding the remote attribute */ 079 protected static final String __PARAM_MAPPING_ATTRIBUTE_PREFIX = "attribute-"; 080 081 /** The logger */ 082 protected static final Logger _LOGGER = LoggerFactory.getLogger(UserPopulationSynchronizableContentsCollection.class); 083 084 private static final int __LDAP_DEFAULT_PAGE_SIZE = 1000; 085 086 /** The DAO for user populations */ 087 protected UserPopulationDAO _userPopulationDAO; 088 /** The service manager */ 089 protected ServiceManager _manager; 090 /** The JSON utils */ 091 protected JSONUtils _jsonUtils; 092 /** The user SCC helper */ 093 protected UserSCCHelper _userSCCHelper; 094 /** The delete user component */ 095 protected DeleteUserComponent _deleteUserComponent; 096 097 /** Mapping of the metadata with source data */ 098 protected Map<String, Map<String, List<String>>> _mapping; 099 /** Synchronized fields */ 100 protected Set<String> _syncFields; 101 /** External fields */ 102 protected Set<String> _extFields; 103 104 @Override 105 public void service(ServiceManager manager) throws ServiceException 106 { 107 super.service(manager); 108 _userPopulationDAO = (UserPopulationDAO) manager.lookup(UserPopulationDAO.ROLE); 109 _jsonUtils = (JSONUtils) manager.lookup(JSONUtils.ROLE); 110 _userSCCHelper = (UserSCCHelper) manager.lookup(UserSCCHelper.ROLE); 111 _deleteUserComponent = (DeleteUserComponent) manager.lookup(DeleteUserComponent.ROLE); 112 _manager = manager; 113 } 114 115 @Override 116 protected void configureDataSource(Configuration configuration) throws ConfigurationException 117 { 118 _mapping = new HashMap<>(); 119 _syncFields = new HashSet<>(); 120 _extFields = new HashSet<>(); 121 String mappingAsString = (String) getParameterValues().get(__PARAM_MAPPING); 122 if (StringUtils.isNotEmpty(mappingAsString)) 123 { 124 List<Object> mappingAsList = _jsonUtils.convertJsonToList(mappingAsString); 125 for (Object object : mappingAsList) 126 { 127 @SuppressWarnings("unchecked") 128 Map<String, Object> field = (Map<String, Object>) object; 129 String metadataRef = (String) field.get(__PARAM_MAPPING_METADATA_REF); 130 131 String prefix = __PARAM_MAPPING_ATTRIBUTE_PREFIX; 132 for (String prefixedUserDirectoryKey : _getUserDirectoryKeys(field, prefix)) 133 { 134 String userDirectoryKey = prefixedUserDirectoryKey.substring(prefix.length()); 135 if (!_mapping.containsKey(userDirectoryKey)) 136 { 137 _mapping.put(userDirectoryKey, new HashMap<>()); 138 } 139 140 String[] attributes = ((String) field.get(prefixedUserDirectoryKey)).split(","); 141 142 Map<String, List<String>> userDirectoryMapping = _mapping.get(userDirectoryKey); 143 userDirectoryMapping.put(metadataRef, Arrays.asList(attributes)); 144 } 145 146 boolean isSynchronized = field.containsKey(__PARAM_MAPPING_SYNCHRO) ? (Boolean) field.get(__PARAM_MAPPING_SYNCHRO) : false; 147 if (isSynchronized) 148 { 149 _syncFields.add(metadataRef); 150 } 151 else 152 { 153 _extFields.add(metadataRef); 154 } 155 } 156 } 157 } 158 159 @Override 160 protected void configureSearchModel() 161 { 162 ContentType contentType = _contentTypeEP.getExtension(getContentType()); 163 if (_mapping.size() > 0) 164 { 165 for (String metadataName : _mapping.get(_mapping.keySet().iterator().next()).keySet()) 166 { 167 MetadataDefinition metadataDef = contentType.getMetadataDefinition(metadataName); 168 I18nizableText label; 169 if (metadataDef != null) 170 { 171 label = metadataDef.getLabel(); 172 } 173 else 174 { 175 label = new I18nizableText(metadataName); 176 _LOGGER.error("In the SCC '{}' ({}), the mapped metadata '{}' for the content type '{}' doesn't exist.", getLabel(), getId(), metadataName, getContentType()); 177 } 178 _searchModelConfiguration.addCriterion(metadataName, label); 179 _searchModelConfiguration.addColumn(metadataName, label, false); 180 } 181 } 182 } 183 184 @Override 185 protected List<ModifiableDefaultContent> _internalPopulate(Logger logger) 186 { 187 List<ModifiableDefaultContent> contents = new ArrayList<>(); 188 189 UserPopulation population = _userPopulationDAO.getUserPopulation(getPopulationId()); 190 191 for (UserDirectory userDirectory : population.getUserDirectories()) 192 { 193 Map<String, Object> searchParams = new HashMap<>(); 194 searchParams.put("userDirectory", userDirectory); 195 contents.addAll(_importOrSynchronizeContents(searchParams, false, logger)); 196 } 197 198 return contents; 199 } 200 201 /** 202 * Search contents from a LDAP user directory of the population. 203 * To avoid code duplication and useless operations, we return a {@link Map}<{@link String}, {@link Map}<{@link String}, {@link Object}>> 204 * if getRemoteValues is set to false and {@link Map}<{@link String}, {@link Map}<{@link String}, {@link List}<{@link Object}>>> 205 * if remoteValues is true. 206 * Without this operation, we have to duplicate the code of searchLDAP and _internalSearch methods. 207 * @param userDirectory The LDAP user directory 208 * @param parameters Parameters for the search 209 * @param offset Begin of the search 210 * @param limit Number of results 211 * @param logger The logger 212 * @param getRemoteValues if true, values are organized by the metadata mapping 213 * @return Contents found in LDAP 214 */ 215 @SuppressWarnings("unchecked") 216 protected Map<String, Map<String, Object>> searchLDAP(LdapUserDirectory userDirectory, Map<String, Object> parameters, int offset, int limit, Logger logger, boolean getRemoteValues) 217 { 218 Map<String, Map<String, Object>> results = new HashMap<>(); 219 220 Map<String, Object> ldapParameterValues = userDirectory.getParameterValues(); 221 String dataSourceId = (String) ldapParameterValues.get(LdapUserDirectory.PARAM_DATASOURCE_ID); 222 String relativeDN = (String) ldapParameterValues.get(LdapUserDirectory.PARAM_USERS_RELATIVE_DN); 223 String filter = (String) ldapParameterValues.get(LdapUserDirectory.PARAM_USERS_OBJECT_FILTER); 224 String searchScope = (String) ldapParameterValues.get(LdapUserDirectory.PARAM_USERS_SEARCH_SCOPE); 225 String loginAttr = (String) ldapParameterValues.get(LdapUserDirectory.PARAM_USERS_LOGIN_ATTRIBUTE); 226 227 Map<String, List<String>> udMapping = _mapping.get(userDirectory.getId()); 228 if (udMapping == null) 229 { 230 udMapping = new HashMap<>(); 231 } 232 udMapping.put(getLoginMetadata(), Collections.singletonList(loginAttr)); 233 234 // If first name metadata is set 235 String firstNameMetadata = getFirstNameMetadata(); 236 if (StringUtils.isNotBlank(firstNameMetadata)) 237 { 238 String firstNameAttr = (String) ldapParameterValues.get(LdapUserDirectory.PARAM_USERS_FIRSTNAME_ATTRIBUTE); 239 udMapping.put(firstNameMetadata, Collections.singletonList(firstNameAttr)); 240 } 241 242 // If last name metadata is set 243 String lastNameMetadata = getLastNameMetadata(); 244 if (StringUtils.isNotBlank(lastNameMetadata)) 245 { 246 String lastNameAttr = (String) ldapParameterValues.get(LdapUserDirectory.PARAM_USERS_LASTNAME_ATTRIBUTE); 247 udMapping.put(lastNameMetadata, Collections.singletonList(lastNameAttr)); 248 } 249 250 // If email metadata is set 251 String emailMetadata = getEmailMetadata(); 252 if (StringUtils.isNotBlank(emailMetadata)) 253 { 254 String emailAttr = (String) ldapParameterValues.get(LdapUserDirectory.PARAM_USERS_EMAIL_ATTRIBUTE); 255 udMapping.put(emailMetadata, Collections.singletonList(emailAttr)); 256 } 257 258 try 259 { 260 LDAPCollectionHelper ldapHelper = (LDAPCollectionHelper) _manager.lookup(LDAPCollectionHelper.ROLE); 261 ldapHelper._delayedInitialize(dataSourceId); 262 263 List<String> filters = new ArrayList<>(); 264 if (StringUtils.isNotEmpty(filter)) 265 { 266 filters.add(filter); 267 } 268 if (parameters != null) 269 { 270 for (String parameterName : parameters.keySet()) 271 { 272 filters.add(parameterName + "=" + parameters.get(parameterName)); 273 } 274 } 275 String additionalSearchFilter = getAdditionalSearchFilter(); 276 if (StringUtils.isNotEmpty(additionalSearchFilter)) 277 { 278 filters.add(additionalSearchFilter); 279 } 280 281 String filtersReduced = filters.stream().filter(StringUtils::isNotEmpty).map(s -> "(" + s + ")").reduce("", (s1, s2) -> s1 + s2); 282 if (!filtersReduced.isEmpty()) 283 { 284 filtersReduced = "(&" + filtersReduced + ")"; 285 } 286 287 results = ldapHelper.search(getId(), __LDAP_DEFAULT_PAGE_SIZE, relativeDN, filtersReduced, searchScope, offset, limit, udMapping, loginAttr, logger); 288 if (getRemoteValues) 289 { 290 results = (Map<String, Map<String, Object>>) (Object) _sccHelper.organizeRemoteValuesByMetadata(results, udMapping); 291 } 292 _nbError = ldapHelper.getNbErrors(); 293 _hasGlobalError = ldapHelper.hasGlobalError(); 294 } 295 catch (Exception e) 296 { 297 logger.error("An error occured when importing from LDAP UserDirectory", e); 298 } 299 300 return results; 301 } 302 303 @Override 304 protected Map<String, Map<String, Object>> internalSearch(Map<String, Object> parameters, int offset, int limit, List<Object> sort, Logger logger) 305 { 306 return _internalSearch(parameters, offset, limit, sort, logger, false); 307 } 308 309 /** 310 * Internal search 311 * @param parameters the search parameters 312 * @param offset starting index 313 * @param limit max number of results 314 * @param sort not used 315 * @param logger the logger 316 * @param getRemoteValues to get remote values or not 317 * @return The search result 318 */ 319 private Map<String, Map<String, Object>> _internalSearch(Map<String, Object> parameters, int offset, int limit, List<Object> sort, Logger logger, boolean getRemoteValues) 320 { 321 Map<String, Map<String, Object>> results = new LinkedHashMap<>(); 322 323 List<UserDirectory> userDirectories = new ArrayList<>(); 324 if (parameters.containsKey("userDirectory")) 325 { 326 userDirectories.add((UserDirectory) parameters.get("userDirectory")); 327 parameters.remove("userDirectory"); 328 } 329 else 330 { 331 UserPopulation population = _userPopulationDAO.getUserPopulation(getPopulationId()); 332 userDirectories = population.getUserDirectories(); 333 } 334 335 for (UserDirectory userDirectory : userDirectories) 336 { 337 if (userDirectory instanceof LdapUserDirectory) 338 { 339 // Sort is ignored for LDAP 340 results.putAll(searchLDAP((LdapUserDirectory) userDirectory, parameters, offset, limit, logger, getRemoteValues)); 341 } 342 else if (userDirectory instanceof JdbcUserDirectory) 343 { 344 // TODO handle SQL case 345 // remoteValuesByContent = searchSQL((JdbcUserDirectory) userDirectory, logger); 346 } 347 } 348 349 return results; 350 } 351 352 @Override 353 protected boolean _fillContent(Map<String, List<Object>> remoteValues, ModifiableDefaultContent content, boolean create, Logger logger) 354 { 355 boolean hasChanges = super._fillContent(remoteValues, content, create, logger); 356 357 String newLogin = remoteValues.get(getIdField()).get(0).toString(); 358 hasChanges = _userSCCHelper.synchronizeUserMetadata(newLogin, getPopulationId(), content, logger) || hasChanges; 359 360 return hasChanges; 361 } 362 363 @Override 364 @SuppressWarnings("unchecked") 365 protected Map<String, Map<String, List<Object>>> getRemoteValues(Map<String, Object> parameters, Logger logger) 366 { 367 return (Map<String, Map<String, List<Object>>>) (Object) _internalSearch(parameters, 0, Integer.MAX_VALUE, null, logger, true); 368 } 369 370 /** 371 * Get the id of the user population 372 * @return The id of user population 373 */ 374 public String getPopulationId() 375 { 376 return (String) getParameterValues().get(__PARAM_POPULATION_ID); 377 } 378 379 @Override 380 public String getIdField() 381 { 382 return getLoginMetadata(); 383 } 384 385 /** 386 * Get the metadata name for the login 387 * @return The the metadata name for the login 388 */ 389 public String getLoginMetadata() 390 { 391 return UserSCCHelper.USER_UNIQUE_ID_METADATA_NAME; 392 } 393 394 /** 395 * Get the metadata name for the first name 396 * @return The the metadata name for the first name 397 */ 398 public String getFirstNameMetadata() 399 { 400 return (String) getParameterValues().get(__PARAM_FIRSTNAME_METADATA_NAME); 401 } 402 403 /** 404 * Get the metadata name for the last name 405 * @return The the metadata name for the last name 406 */ 407 public String getLastNameMetadata() 408 { 409 return (String) getParameterValues().get(__PARAM_LASTNAME_METADATA_NAME); 410 } 411 412 /** 413 * Get the metadata name for the email 414 * @return The the metadata name for the email 415 */ 416 public String getEmailMetadata() 417 { 418 return (String) getParameterValues().get(__PARAM_EMAIL_METADATA_NAME); 419 } 420 421 /** 422 * Get the additional filter for searching 423 * @return The additional filter for searching 424 */ 425 public String getAdditionalSearchFilter() 426 { 427 return (String) getParameterValues().get(__PARAM_ADDITIONAL_SEARCH_FILTER); 428 } 429 430 @Override 431 public Set<String> getExternalOnlyFields(Map<String, Object> additionalParameters) 432 { 433 return _extFields; 434 } 435 436 @Override 437 public Set<String> getLocalAndExternalFields(Map<String, Object> additionalParameters) 438 { 439 return _syncFields; 440 } 441 442 private Set<String> _getUserDirectoryKeys(Map<String, Object> field, String prefix) 443 { 444 return field.keySet().stream() 445 .filter(name -> name.startsWith(prefix)) 446 .collect(Collectors.toSet()); 447 } 448 449 @Override 450 protected Map<String, Object> putIdParameter(String idValue) 451 { 452 Map<String, Object> parameters = new HashMap<>(); 453 454 for (String userDirectory : _mapping.keySet()) 455 { 456 List<String> remoteKeys = _mapping.get(userDirectory).get(getIdField()); 457 if (remoteKeys != null && remoteKeys.size() > 0) 458 { 459 parameters.put(userDirectory + "$" + remoteKeys.get(0), idValue); 460 } 461 } 462 463 return parameters; 464 } 465 466 @Override 467 protected void deleteUnexistingContents(Logger logger) 468 { 469 String query = _getContentPathQuery(null, null, null); 470 AmetysObjectIterable<ModifiableDefaultContent> contents = _resolver.query(query); 471 472 List<Content> contentsToRemove = _getContentsToRemove(contents); 473 if (!contentsToRemove.isEmpty()) 474 { 475 _nbDeletedContents += _deleteUserComponent.deleteContentsWithLog(contentsToRemove, MapUtils.EMPTY_SORTED_MAP, MapUtils.EMPTY_SORTED_MAP, logger); 476 } 477 } 478}