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