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}&lt;{@link String}, {@link Map}&lt;{@link String}, {@link Object}&gt;&gt; 
193     * 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; 
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}