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