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