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