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