001/*
002 *  Copyright 2018 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.HashMap;
020import java.util.LinkedHashMap;
021import java.util.List;
022import java.util.Map;
023import java.util.Optional;
024import java.util.Set;
025
026import org.apache.avalon.framework.service.ServiceException;
027import org.apache.avalon.framework.service.ServiceManager;
028import org.apache.commons.lang3.StringUtils;
029import org.apache.commons.lang3.tuple.Triple;
030import org.slf4j.Logger;
031
032import org.ametys.cms.content.references.OutgoingReferencesExtractor;
033import org.ametys.cms.data.ContentSynchronizationResult;
034import org.ametys.cms.repository.Content;
035import org.ametys.cms.repository.ContentQueryHelper;
036import org.ametys.cms.repository.ContentTypeExpression;
037import org.ametys.cms.repository.LanguageExpression;
038import org.ametys.cms.repository.ModifiableContent;
039import org.ametys.cms.repository.WorkflowAwareContent;
040import org.ametys.cms.workflow.AbstractContentWorkflowComponent;
041import org.ametys.cms.workflow.EditContentFunction;
042import org.ametys.core.schedule.progression.ContainerProgressionTracker;
043import org.ametys.core.user.CurrentUserProvider;
044import org.ametys.plugins.contentio.synchronize.SynchronizableContentsCollectionDataProvider;
045import org.ametys.plugins.contentio.synchronize.impl.SQLSynchronizableContentsCollection;
046import org.ametys.plugins.contentio.synchronize.workflow.EditSynchronizedContentFunction;
047import org.ametys.plugins.repository.AmetysObjectIterable;
048import org.ametys.plugins.repository.AmetysObjectIterator;
049import org.ametys.plugins.repository.data.external.ExternalizableDataProvider.ExternalizableDataStatus;
050import org.ametys.plugins.repository.data.holder.ModifiableModelLessDataHolder;
051import org.ametys.plugins.repository.data.holder.values.SynchronizationContext;
052import org.ametys.plugins.repository.data.holder.values.SynchronizationResult;
053import org.ametys.plugins.repository.query.expression.AndExpression;
054import org.ametys.plugins.repository.query.expression.Expression;
055import org.ametys.plugins.repository.query.expression.Expression.Operator;
056import org.ametys.plugins.repository.query.expression.ExpressionContext;
057import org.ametys.plugins.repository.query.expression.StringExpression;
058import org.ametys.plugins.userdirectory.DeleteOrgUnitComponent;
059import org.ametys.plugins.userdirectory.OrganisationChartPageHandler;
060import org.ametys.plugins.userdirectory.UserDirectoryHelper;
061import org.ametys.plugins.workflow.AbstractWorkflowComponent;
062import org.ametys.runtime.i18n.I18nizableText;
063import org.ametys.runtime.model.View;
064import org.ametys.runtime.model.type.ModelItemTypeConstants;
065
066import com.opensymphony.workflow.InvalidActionException;
067import com.opensymphony.workflow.WorkflowException;
068
069/**
070 * Synchronizable collection for UD Orgunits
071 */
072public class SQLSynchronizableUDOrgunitCollection extends SQLSynchronizableContentsCollection
073{
074    /** The internal data name for orgUnit remote sql id */
075    public static final String ORGUNIT_REMOTE_ID_INTERNAL_DATA = "orgunit-remote-id";
076    
077    /** The result map key for number of deleted contents */
078    public static final String RESULT_NB_SYNCHRONIZED_ORGUNITS_RELATIONS = "nbSynchronizedOrgUnitsRelations";
079
080    private static final String __PARAM_SQL_TABLE_USER = "tableNameUser";
081    private static final String __PARAM_SQL_ORGUNIT_JOIN_COLUMN_NAME = "orgUnitJoinColumnName";
082    private static final String __PARAM_LOGIN_USER_ATTRIBUTE_NAME = "loginUser";
083    private static final String __PARAM_SQL_LOGIN_USER_COLUMN_NAME = "loginColumnName";
084    private static final String __PARAM_SQL_ROLE_USER_COLUMN_NAME = "roleColumnName";
085    private static final String __PARAM_SQL_ORGUNIT_REMOTE_ID_COLUMN_NAME = "orgunitRemoteIdColumnName";
086    private static final String __PARAM_SQL_USER_SORT_COLUMN_NAME = "sortColumnName";
087    private static final String __PARAM_SQL_USER_SORT_PREVAIL_NAME = "sortPrevail";
088    
089    /** The map which link orgunit with this parent */
090    protected Map<String, String> _orgUnitParents;
091    
092    /** The map which link users (userId and role) to their orgunits */
093    protected Map<String, Map<String, String>> _usersByOrgUnitId;
094    
095    /** The map which link orgunit with sql remote ids */
096    protected Map<String, String> _orgUnitRemoteIds;
097    
098    
099    /** Number of updated contents for parent-child org unit relation */
100    protected Integer _nbSynchronizedOrgUnit;
101    
102    /** The sql user search DAO */
103    protected SQLUserSearchDAO _sqlUserDAO;
104    
105    /** The organisation chart page handler */
106    protected OrganisationChartPageHandler _orgChartPageHandler;
107    
108    /** The current user provider */
109    protected CurrentUserProvider _userProvider;
110    
111    /** The OutgoingReferences extractor */
112    protected OutgoingReferencesExtractor _outgoingReferencesExtractor;
113    
114    /** The delete orgUnit component */
115    protected DeleteOrgUnitComponent _deleteOrgUnitComponent;
116
117    @Override
118    public void service(ServiceManager smanager) throws ServiceException
119    {
120        super.service(smanager);
121        _sqlUserDAO = (SQLUserSearchDAO) smanager.lookup(SQLUserSearchDAO.ROLE);
122        _orgChartPageHandler = (OrganisationChartPageHandler) smanager.lookup(OrganisationChartPageHandler.ROLE);
123        _userProvider = (CurrentUserProvider) smanager.lookup(CurrentUserProvider.ROLE);
124        _outgoingReferencesExtractor = (OutgoingReferencesExtractor) smanager.lookup(OutgoingReferencesExtractor.ROLE);
125        _deleteOrgUnitComponent = (DeleteOrgUnitComponent) smanager.lookup(DeleteOrgUnitComponent.ROLE);
126    }
127    
128    public boolean handleRightAssignmentContext()
129    {
130        return false;
131    }
132    
133    /**
134     * Get the name of user SQL table
135     * @return The user SQL table name
136     */
137    public String getUserTableName()
138    {
139        return (String) getParameterValues().get(__PARAM_SQL_TABLE_USER);
140    }
141    
142    /**
143     * Get the name of the orgunit join column name of the user table 
144     * @return The name of the orgunit join column name
145     */
146    public String getOrgunitJoinColumnNameForUser()
147    {
148        return (String) getParameterValues().get(__PARAM_SQL_ORGUNIT_JOIN_COLUMN_NAME);
149    }
150    
151    /**
152     * Get the login user attribute name
153     * @return The login user attribute name
154     */
155    public String getLoginUserAttributeName()
156    {
157        return (String) getParameterValues().get(__PARAM_LOGIN_USER_ATTRIBUTE_NAME);
158    }
159    
160    /**
161     * Get the login user column name
162     * @return The login user column name
163     */
164    public String getLoginUserColumnName()
165    {
166        return (String) getParameterValues().get(__PARAM_SQL_LOGIN_USER_COLUMN_NAME);
167    }
168    
169    /**
170     * Get the role user column name
171     * @return The role user column name
172     */
173    public String getRoleUserColumnName()
174    {
175        return (String) getParameterValues().get(__PARAM_SQL_ROLE_USER_COLUMN_NAME);
176    }
177    
178    /**
179     * Get the user sort column name
180     * @return The user sort column name
181     */
182    public String getUserSortColumnName()
183    {
184        return (String) getParameterValues().get(__PARAM_SQL_USER_SORT_COLUMN_NAME);
185    }
186    
187    /**
188     * Get the orgunit remote id column name
189     * @return The orgunit remote id column name
190     */
191    public String getOrgUnitRemoteIdColumnName()
192    {
193        return (String) getParameterValues().get(__PARAM_SQL_ORGUNIT_REMOTE_ID_COLUMN_NAME);
194    }
195    
196    /**
197     * True if the SQL sort for users prevail
198     * @return true if the SQL sort for users prevail
199     */
200    public boolean isUserSortPrevail()
201    {
202        return Boolean.valueOf((String) getParameterValues().get(__PARAM_SQL_USER_SORT_PREVAIL_NAME));
203    }
204    
205    @Override
206    protected Map<String, Object> _getSearchParameters(Map<String, Object> parameters, int offset, int limit, List<Object> sort, List<String> columns)
207    {
208        // Add the sql column name for the orgunit id. 
209        // It's for setting the ametys-internal:orgunit-remote-id and retrieve easily the orgUnit content with this Id 
210        String orgUnitIdColumnName = getOrgUnitRemoteIdColumnName();
211        if (!columns.contains(orgUnitIdColumnName))
212        {
213            columns.add(orgUnitIdColumnName);
214        }
215        
216        return super._getSearchParameters(parameters, offset, limit, sort, columns);
217    }
218    
219    @Override
220    protected Map<String, Map<String, Object>> internalSearch(Map<String, Object> searchParameters, int offset, int limit, List<Object> sort, Logger logger)
221    {
222        Map<String, Map<String, Object>> internalSearch = super.internalSearch(searchParameters, offset, limit, sort, logger);
223
224        // Fill _orgUnitRemoteIds and _orgUnitParents maps with search results
225        String orgUnitRemoteIdColumnName = getOrgUnitRemoteIdColumnName();
226        for (String orgUnitIdValue : internalSearch.keySet())
227        {
228            Map<String, Object> orgUnitValues = internalSearch.get(orgUnitIdValue);
229            _orgUnitRemoteIds.put(orgUnitIdValue, orgUnitValues.get(orgUnitRemoteIdColumnName).toString());
230            
231            Map<String, List<String>> mapping = getMapping();
232            if (mapping.containsKey(OrganisationChartPageHandler.PARENT_ORGUNIT_ATTRIBUTE_NAME))
233            {
234                // We take the first because it's no sense to defined two sql columns to define orgunit parents
235                String parentColumn = mapping.get(OrganisationChartPageHandler.PARENT_ORGUNIT_ATTRIBUTE_NAME).get(0); 
236                if (orgUnitValues.containsKey(parentColumn))
237                {
238                    String parentId = Optional.of(orgUnitValues)
239                            .map(v -> v.get(parentColumn))
240                            .map(Object::toString)
241                            .orElse(null); 
242                    _orgUnitParents.put(orgUnitIdValue, parentId);
243                }
244            }
245        }
246        
247        return internalSearch;
248    }
249    
250    @Override
251    protected void _logSynchronizationResult(Logger logger)
252    {
253        super._logSynchronizationResult(logger);
254        logger.info("{} contents have been modified to update the parent-child relations.", _nbSynchronizedOrgUnit);
255    }
256    
257    @Override
258    protected boolean _hasSomethingChanged()
259    {
260        return super._hasSomethingChanged() || _nbSynchronizedOrgUnit > 0;
261    }
262    
263    @Override
264    protected List<ModifiableContent> _internalPopulate(Logger logger, ContainerProgressionTracker progressionTracker)
265    {
266        _orgUnitParents = new HashMap<>();
267        _orgUnitRemoteIds = new HashMap<>();
268        _usersByOrgUnitId = _getUsersByOrgUnit(logger);
269        _nbSynchronizedOrgUnit = 0;
270        
271        List<ModifiableContent> contents = super._internalPopulate(logger, progressionTracker);
272        
273        // All orgunits are imported, now we can set relation with parent orgunit
274        _setContentsRelationWithParentOrgunit(contents, logger);
275        
276        return contents;
277    }
278    
279    /**
280     * Retrieves the user attributes for all org units
281     * @param logger The logger
282     * @return the org units' users
283     */
284    protected Map<String, Map<String, String>> _getUsersByOrgUnit(Logger logger)
285    {
286        Map<String, Map<String, String>> orgUnitsUsers = new HashMap<>();
287
288        Map<String, List<String>> mapping = getMapping();
289        String idField = getIdField();
290        List<String> orgUnitKeys = mapping.get(idField);
291        if (orgUnitKeys != null && orgUnitKeys.size() > 0)
292        {
293            String orgUnitKey = orgUnitKeys.get(0);
294            Map<String, Object> userParameters = _getSearchUserParameters(orgUnitKey, logger);
295            List<Map<String, Object>> searchUserList = _sqlUserDAO.searchUser(userParameters, getDataSourceId());
296
297            String loginColumnName = getLoginUserColumnName();
298            String roleColumnName = getRoleUserColumnName();
299
300            List<String> userColumns = new ArrayList<>();
301            userColumns.add(loginColumnName);
302            userColumns.add(orgUnitKey);
303            if (StringUtils.isNotBlank(roleColumnName))
304            {
305                userColumns.add(roleColumnName);
306            }
307
308            for (Map<String, Object> userMap : searchUserList)
309            {
310                Map<String, Object> normalizedUserMap = _getNormalizedSearchResult(userColumns, userMap);
311                Optional<Triple<String, String, String>> orgUnitUser = _getUserByOrgUnit(orgUnitKey, loginColumnName, roleColumnName, normalizedUserMap, logger);
312                if (orgUnitUser.isPresent())
313                {
314                    Triple<String, String, String> triple = orgUnitUser.get();
315                    String orgUnitId = triple.getLeft();
316                    Map<String, String> orgUnitUsers = orgUnitsUsers.computeIfAbsent(orgUnitId, __ -> new LinkedHashMap<>());
317                    orgUnitUsers.put(triple.getMiddle(), triple.getRight());
318                }
319            }
320        }
321
322        return orgUnitsUsers;
323    }
324    
325    /**
326     * Retrieves a {@link Triple} containing the orgunit id, and the user's login and role for the given normalized user
327     * @param orgUnitKey the orgUnit key
328     * @param loginColumnName the login column name
329     * @param roleColumnName the role column name
330     * @param normalizedUser the normalized user
331     * @param logger the logger
332     * @return the user info as a {@link Triple}
333     */
334    protected Optional<Triple<String, String, String>> _getUserByOrgUnit(String orgUnitKey, String loginColumnName, String roleColumnName, Map<String, Object> normalizedUser, Logger logger)
335    {
336        String loginValue = (normalizedUser.get(loginColumnName) == null) ? null : String.valueOf(normalizedUser.get(loginColumnName)); 
337        String orgUnitIdValue = normalizedUser.get(orgUnitKey).toString();
338        
339        if (StringUtils.isNotBlank(loginValue))
340        {
341            String roleValue = null;
342            if (StringUtils.isNotBlank(roleColumnName))
343            {
344                roleValue = (normalizedUser.get(roleColumnName) == null) ? null :  String.valueOf(normalizedUser.get(roleColumnName));
345            }
346            
347            return Optional.of(Triple.of(orgUnitIdValue, loginValue, roleValue));
348        }
349        else
350        {
351            logger.warn("Can't add user to orgunit '" + orgUnitIdValue + "' because the login value is blank ...");
352            return Optional.empty();
353        }
354    }
355    
356    @Override
357    protected Optional<ModifiableContent> _importOrSynchronizeContent(String idValue, String lang, Map<String, List<Object>> remoteValues, boolean forceImport, Logger logger)
358    {
359        // Do not set relation with parent orgunit now, while they might miss some that are not yet created
360        remoteValues.remove(OrganisationChartPageHandler.PARENT_ORGUNIT_ATTRIBUTE_NAME);
361        
362        return super._importOrSynchronizeContent(idValue, lang, remoteValues, forceImport, logger);
363    }
364    
365    @Override
366    public ContentSynchronizationResult additionalCommonOperations(ModifiableContent content, Map<String, Object> additionalParameters, Logger logger)
367    {
368        ContentSynchronizationResult result = super.additionalCommonOperations(content, additionalParameters, logger);
369        
370        try
371        {
372            SynchronizationResult additionalResult = new SynchronizationResult();
373            String orgUnitIdValue = content.getValue(getIdField());
374            ModifiableModelLessDataHolder internalDataHolder = content.getInternalDataHolder();
375            
376            String oldValue = internalDataHolder.getValueOfType(ORGUNIT_REMOTE_ID_INTERNAL_DATA, ModelItemTypeConstants.STRING_TYPE_ID);
377            String value = _orgUnitRemoteIds.get(orgUnitIdValue);
378            
379            if (!value.equals(oldValue))
380            {
381                internalDataHolder.setValue(ORGUNIT_REMOTE_ID_INTERNAL_DATA, value);
382                additionalResult.setHasChanged(true);
383            }
384            
385            result.aggregateResult(additionalResult);
386        }
387        catch (Exception e)
388        {
389            _nbError++;
390            logger.error("An error occurred while importing or synchronizing orgunit '{}' and setting its remote id.", content, e);
391        }
392        
393        return result;
394    }
395    
396    @Override
397    protected Map<String, Object> getAdditionalAttributeValues(String idValue, Content content, Map<String, Object> additionalParameters, boolean create, Logger logger)
398    {
399        Map<String, Object> additionalRemoteValues = super.getAdditionalAttributeValues(idValue, content, additionalParameters, create, logger);
400        
401        List<Map<String, Object>> orgUnitUsers = _getOrgUnitUsers(idValue, content.getLanguage());
402        if (!orgUnitUsers.isEmpty())
403        {
404            additionalRemoteValues.put(OrganisationChartPageHandler.ORGUNIT_USERS_ATTRIBUTE_NAME, orgUnitUsers);
405        }
406        
407        return additionalRemoteValues;
408    }
409    
410    /**
411     * Retrieves the users of the given orgunit
412     * @param orgUnitId the orgunit identifier
413     * @param lang the language of the orgunit
414     * @return the users linked to the given orgunit
415     */
416    protected List<Map<String, Object>> _getOrgUnitUsers(String orgUnitId, String lang)
417    {
418        if (_usersByOrgUnitId.containsKey(orgUnitId))
419        {
420            List<Map<String, Object>> users = new ArrayList<>();
421            for (Map.Entry<String, String> user : _usersByOrgUnitId.get(orgUnitId).entrySet())
422            {
423                Map<String, Object> orgUnitUser = new HashMap<>();
424                
425                String loginValue = user.getKey();
426                Content userContent = _getUserContent(loginValue, lang);
427                
428                if (userContent != null)
429                {
430                    String roleValue = user.getValue();
431                    orgUnitUser.put(OrganisationChartPageHandler.ORGUNIT_USER_ROLE_ATTRIBUTE_NAME, roleValue);
432                    orgUnitUser.put(OrganisationChartPageHandler.ORGUNIT_USER_ATTRIBUTE_NAME, userContent);
433                    
434                    users.add(orgUnitUser);
435                }
436            }
437            
438            return users;
439        }
440        else
441        {
442            return List.of();
443        }
444    }
445    
446    /**
447     * Set all orgunit parents relation for each synchronized content
448     * @param orgUnitContents the synchronized content
449     * @param logger the logger
450     */
451    protected void _setContentsRelationWithParentOrgunit(List<ModifiableContent> orgUnitContents, Logger logger)
452    {
453        for (ModifiableContent orgUnitContent : orgUnitContents)
454        {
455            try
456            {
457                if (orgUnitContent instanceof WorkflowAwareContent)
458                {
459                    I18nizableText commentText = new I18nizableText("plugin.user-directory", "PLUGINS_USER_DIRECTORY_WORKFLOW_ACTION_EDIT_ORGUNIT_REFERENCE_MSG");
460                    String comment = _i18nUtils.translate(commentText, orgUnitContent.getLanguage());
461
462                    View view = View.of(orgUnitContent.getModel(), OrganisationChartPageHandler.PARENT_ORGUNIT_ATTRIBUTE_NAME);
463
464                    Map<String, Object> values = new HashMap<>();
465                    String orgUnitIdValue = orgUnitContent.getValue(getIdField());
466                    String newParentSynchroId = _orgUnitParents.get(orgUnitIdValue);
467                    Content newParentContent = newParentSynchroId != null ? _getOrgUnitContentFromRemoteId(newParentSynchroId, orgUnitContent.getLanguage(), logger) : null;
468                    values.put(OrganisationChartPageHandler.PARENT_ORGUNIT_ATTRIBUTE_NAME, newParentContent);
469                    
470                    SynchronizationContext synchronizationContext = SynchronizationContext.newInstance()
471                                                                                          .withStatus(ExternalizableDataStatus.EXTERNAL)
472                                                                                          .withExternalizableDataContextEntry(SynchronizableContentsCollectionDataProvider.SCC_ID_CONTEXT_KEY, getId())
473                                                                                          .withIncompatibleValuesIgnored(true);
474                    
475                    if (orgUnitContent.hasDifferences(view, values, synchronizationContext))
476                    {
477                        Map<String, Object> inputs = new HashMap<>();
478                        if (StringUtils.isNotEmpty(comment))
479                        {
480                            inputs.put("comment", comment);
481                        }
482    
483                        Map<String, Object> parameters = new HashMap<>();
484    
485                        parameters.put(EditContentFunction.VIEW, view);
486                        parameters.put(EditContentFunction.VALUES_KEY, values);
487                        parameters.put(EditContentFunction.QUIT, true);
488                        inputs.put(AbstractWorkflowComponent.CONTEXT_PARAMETERS_KEY, parameters);
489    
490                        inputs.put(EditSynchronizedContentFunction.SYNCHRO_INVERT_EDIT_ACTION_ID_KEY, getSynchronizeActionId());
491    
492                        Map<String, Object> actionResult = _contentWorkflowHelper.doAction((WorkflowAwareContent) orgUnitContent, getSynchronizeActionId(), inputs);
493                        if ((boolean) actionResult.getOrDefault(AbstractContentWorkflowComponent.HAS_CHANGED_KEY, false))
494                        {
495                            _nbSynchronizedOrgUnit++;
496                        }
497                    }
498                }
499            }
500            catch (WorkflowException | InvalidActionException e)
501            {
502                logger.error("An error occurred while updating parent-child relations of org unit {} ", orgUnitContent, e);
503            }
504        }
505    }
506    
507    /**
508     * Get user content from login value
509     * @param loginValue the login value
510     * @param lang the language
511     * @return the user content
512     */
513    protected Content _getUserContent(String loginValue, String lang)
514    {
515        String loginAttribute = getLoginUserAttributeName();
516        Set<String> contentTypes = _contentTypeEP.getSubTypes(UserDirectoryHelper.ABSTRACT_USER_CONTENT_TYPE);
517        
518        Expression ctypeExpression = new ContentTypeExpression(Operator.EQ, contentTypes.toArray(new String[contentTypes.size()]));
519        LanguageExpression languageExpression = new LanguageExpression(Operator.EQ, lang);
520        StringExpression loginExp = new StringExpression(loginAttribute, Operator.EQ, loginValue);
521        
522        Expression userExp = new AndExpression(loginExp, ctypeExpression, languageExpression);
523        String xPathQuery = ContentQueryHelper.getContentXPathQuery(userExp);
524        
525        AmetysObjectIterable<Content> contentQuery = _resolver.query(xPathQuery);
526        AmetysObjectIterator<Content> contentIterator = contentQuery.iterator();
527        if (contentIterator.hasNext())
528        {
529            return contentIterator.next();
530        }
531        
532        return null;
533    }
534    
535    /**
536     * Get orgunit content from the remote Id
537     * @param remoteId the remote Id
538     * @param lang the language
539     * @param logger the logger
540     * @return the orgunit content
541     */
542    protected Content _getOrgUnitContentFromRemoteId(String remoteId, String lang, Logger logger)
543    {
544        Expression ctypeExpression = new ContentTypeExpression(Operator.EQ, UserDirectoryHelper.ORGUNIT_CONTENT_TYPE);
545        StringExpression remoteIdOrgunitExpression = new StringExpression(ORGUNIT_REMOTE_ID_INTERNAL_DATA, Operator.EQ, remoteId, ExpressionContext.newInstance().withInternal(true));
546        LanguageExpression languageExpression = new LanguageExpression(Operator.EQ, lang);
547        
548        Expression collectionExpression = _sccHelper.getCollectionExpression(getId());
549        
550        Expression userExp = new AndExpression(remoteIdOrgunitExpression, ctypeExpression, languageExpression, collectionExpression);
551        String xPathQuery = ContentQueryHelper.getContentXPathQuery(userExp);
552        
553        AmetysObjectIterable<Content> contentQuery = _resolver.query(xPathQuery);
554        AmetysObjectIterator<Content> contentIterator = contentQuery.iterator();
555        if (contentIterator.hasNext())
556        {
557            return contentIterator.next();
558        }
559        
560        return null;
561    }
562    
563    /**
564     * Get the parameters map for user mybatis search
565     * @param orgUnitColumnKey the column name of the orgunit key
566     * @param logger the logger
567     * @return the parameter map
568     */
569    protected Map<String, Object> _getSearchUserParameters(String orgUnitColumnKey, Logger logger)
570    {
571        Map<String, Object> params = new HashMap<>();
572        params.put("loginColumnName", getLoginUserColumnName());
573        params.put("tableUser", getUserTableName());
574        params.put("tableOrgUnit", getTableName());
575        params.put("joinColumnName", getOrgunitJoinColumnNameForUser());
576        params.put("orgUnitColumnKey", orgUnitColumnKey);
577        params.put("orgUnitIdColumnName", getOrgUnitRemoteIdColumnName());
578        
579        String roleUserColumnName = getRoleUserColumnName();
580        if (StringUtils.isNotBlank(roleUserColumnName))
581        {
582            params.put("roleColumnName", roleUserColumnName);
583        }
584        
585        String sortColumnName = getUserSortColumnName();
586        if (StringUtils.isNotBlank(sortColumnName))
587        {
588            params.put("sortColumnName", sortColumnName);
589        }
590
591        return params;
592    }
593    
594    @Override
595    protected int _deleteContents(List<Content> contentsToRemove, Logger logger)
596    {
597        return _deleteOrgUnitComponent.deleteContentsWithLog(contentsToRemove, Map.of(DeleteOrgUnitComponent.SCC_ID_PARAMETERS_KEY, getId()), Map.of(), logger);
598    }
599    
600    @Override
601    public Map<String, Integer> getSynchronizationResult()
602    {
603        Map<String, Integer> result = super.getSynchronizationResult();
604
605        result.put(RESULT_NB_SYNCHRONIZED_ORGUNITS_RELATIONS, _nbSynchronizedOrgUnit);
606
607        return result;
608    }
609}