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.Collections;
020import java.util.HashMap;
021import java.util.HashSet;
022import java.util.List;
023import java.util.Map;
024import java.util.Optional;
025import java.util.Set;
026import java.util.stream.Stream;
027
028import javax.jcr.RepositoryException;
029
030import org.apache.avalon.framework.service.ServiceException;
031import org.apache.avalon.framework.service.ServiceManager;
032import org.apache.commons.collections4.MapUtils;
033import org.apache.commons.lang.StringUtils;
034import org.apache.commons.lang3.ArrayUtils;
035import org.slf4j.Logger;
036
037import org.ametys.cms.ObservationConstants;
038import org.ametys.cms.content.external.ExternalizableMetadataHelper;
039import org.ametys.cms.content.references.OutgoingReferencesExtractor;
040import org.ametys.cms.data.ContentValue;
041import org.ametys.cms.repository.Content;
042import org.ametys.cms.repository.ContentQueryHelper;
043import org.ametys.cms.repository.ContentTypeExpression;
044import org.ametys.cms.repository.LanguageExpression;
045import org.ametys.cms.repository.ModifiableContent;
046import org.ametys.cms.repository.ModifiableDefaultContent;
047import org.ametys.cms.repository.WorkflowAwareContent;
048import org.ametys.cms.workflow.ContentWorkflowHelper;
049import org.ametys.core.observation.Event;
050import org.ametys.core.user.CurrentUserProvider;
051import org.ametys.plugins.contentio.synchronize.expression.CollectionExpression;
052import org.ametys.plugins.contentio.synchronize.impl.SQLSynchronizableContentsCollection;
053import org.ametys.plugins.repository.AmetysObject;
054import org.ametys.plugins.repository.AmetysObjectIterable;
055import org.ametys.plugins.repository.AmetysObjectIterator;
056import org.ametys.plugins.repository.RepositoryConstants;
057import org.ametys.plugins.repository.UnknownAmetysObjectException;
058import org.ametys.plugins.repository.data.holder.group.impl.ModifiableModelAwareRepeater;
059import org.ametys.plugins.repository.data.holder.group.impl.ModifiableModelAwareRepeaterEntry;
060import org.ametys.plugins.repository.jcr.DefaultAmetysObject;
061import org.ametys.plugins.repository.metadata.ModifiableCompositeMetadata;
062import org.ametys.plugins.repository.query.expression.AndExpression;
063import org.ametys.plugins.repository.query.expression.Expression;
064import org.ametys.plugins.repository.query.expression.Expression.Operator;
065import org.ametys.plugins.repository.query.expression.StringExpression;
066import org.ametys.plugins.userdirectory.DeleteOrgUnitComponent;
067import org.ametys.plugins.userdirectory.OrganisationChartPageHandler;
068import org.ametys.plugins.userdirectory.UserDirectoryHelper;
069import org.ametys.plugins.userdirectory.UserDirectoryPageHandler;
070import org.ametys.plugins.userdirectory.expression.RemoteIdOrgunitExpression;
071import org.ametys.runtime.i18n.I18nizableText;
072
073import com.google.common.collect.Maps;
074import com.opensymphony.workflow.WorkflowException;
075
076/**
077 * Synchronizable collection for UD Orgunits
078 */
079public class SQLSynchronizableUDOrgunitCollection extends SQLSynchronizableContentsCollection
080{
081    /** The internal metadata name for orgUnit remote sql id */
082    public static final String ORGUNIT_REMOTE_ID_INTERNAL_METADATA = RepositoryConstants.NAMESPACE_PREFIX_INTERNAL + ":orgunit-remote-id";
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_METADATA_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    
093    /** The map which link orgunit with this parent */
094    protected Map<String, String> _orgUnitParents;
095    
096    /** The map which link orgunit with users (userId and role) */
097    protected Map<String, Map<String, String>> _orgUnitUsers;
098    
099    /** The map which link orgunit with sql remote ids */
100    protected Map<String, String> _orgUnitRemoteIds;
101    
102    /** The set of orgunits to apply changes */
103    protected Set<WorkflowAwareContent> _orgUnitsToApplyChanges;
104    
105    /** The sql user search DAO */
106    protected SQLUserSearchDAO _sqlUserDAO;
107    
108    /** The organisation chart page handler */
109    protected OrganisationChartPageHandler _orgChartPageHandler;
110    
111    /** The content workflow helper */
112    protected ContentWorkflowHelper _contentWorkflowHelper;
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    @Override
124    public void service(ServiceManager smanager) throws ServiceException
125    {
126        super.service(smanager);
127        _sqlUserDAO = (SQLUserSearchDAO) smanager.lookup(SQLUserSearchDAO.ROLE);
128        _orgChartPageHandler = (OrganisationChartPageHandler) smanager.lookup(OrganisationChartPageHandler.ROLE);
129        _contentWorkflowHelper = (ContentWorkflowHelper) smanager.lookup(ContentWorkflowHelper.ROLE);
130        _userProvider = (CurrentUserProvider) smanager.lookup(CurrentUserProvider.ROLE);
131        _outgoingReferencesExtractor = (OutgoingReferencesExtractor) smanager.lookup(OutgoingReferencesExtractor.ROLE);
132        _deleteOrgUnitComponent = (DeleteOrgUnitComponent) smanager.lookup(DeleteOrgUnitComponent.ROLE);
133    }
134    
135    public boolean handleRightAssignmentContext()
136    {
137        return false;
138    }
139    
140    /**
141     * Get the name of user SQL table
142     * @return The user SQL table name
143     */
144    public String getUserTableName()
145    {
146        return (String) getParameterValues().get(__PARAM_SQL_TABLE_USER);
147    }
148    
149    /**
150     * Get the name of the orgunit join column name of the user table 
151     * @return The name of the orgunit join column name
152     */
153    public String getOrgunitJoinColumnNameForUser()
154    {
155        return (String) getParameterValues().get(__PARAM_SQL_ORGUNIT_JOIN_COLUMN_NAME);
156    }
157    
158    /**
159     * Get the login user metadata name
160     * @return The login user metadata name
161     */
162    public String getLoginUserMetadataName()
163    {
164        return (String) getParameterValues().get(__PARAM_LOGIN_USER_METADATA_NAME);
165    }
166    
167    /**
168     * Get the login user column name
169     * @return The login user column name
170     */
171    public String getLoginUserColumnName()
172    {
173        return (String) getParameterValues().get(__PARAM_SQL_LOGIN_USER_COLUMN_NAME);
174    }
175    
176    /**
177     * Get the role user column name
178     * @return The role user column name
179     */
180    public String getRoleUserColumnName()
181    {
182        return (String) getParameterValues().get(__PARAM_SQL_ROLE_USER_COLUMN_NAME);
183    }
184    
185    /**
186     * Get the user sort column name
187     * @return The user sort column name
188     */
189    public String getUserSortColumnName()
190    {
191        return (String) getParameterValues().get(__PARAM_SQL_USER_SORT_COLUMN_NAME);
192    }
193    
194    /**
195     * Get the orgunit remote id column name
196     * @return The orgunit remote id column name
197     */
198    public String getOrgUnitRemoteIdColumnName()
199    {
200        return (String) getParameterValues().get(__PARAM_SQL_ORGUNIT_REMOTE_ID_COLUMN_NAME);
201    }
202    
203    /**
204     * True if the SQL sort for users prevail
205     * @return true if the SQL sort for users prevail
206     */
207    public boolean isUserSortPrevail()
208    {
209        return Boolean.valueOf((String) getParameterValues().get(__PARAM_SQL_USER_SORT_PREVAIL_NAME));
210    }
211    
212    @Override
213    protected Map<String, Object> _getSearchParameters(Map<String, Object> parameters, int offset, int limit, List<Object> sort, List<String> columns)
214    {
215        // Add the sql column name for the orgunit id. 
216        // It's for setting the ametys-internal:orgunit-remote-id and retrieve easily the orgUnit content with this Id 
217        String orgUnitIdColumnName = getOrgUnitRemoteIdColumnName();
218        if (!columns.contains(orgUnitIdColumnName))
219        {
220            columns.add(orgUnitIdColumnName);
221        }
222        
223        return super._getSearchParameters(parameters, offset, limit, sort, columns);
224    }
225    
226    @Override
227    protected Map<String, Map<String, Object>> internalSearch(Map<String, Object> parameters, int offset, int limit, List<Object> sort, Logger logger)
228    {
229        Map<String, Map<String, Object>> internalSearch = super.internalSearch(parameters, offset, limit, sort, logger);
230
231        // Fill _orgUnitRemoteIds and _orgUnitParents maps with search results
232        String orgUnitRemoteIdColumnName = getOrgUnitRemoteIdColumnName();
233        for (String orgUnitIdValue : internalSearch.keySet())
234        {
235            Map<String, Object> orgUnitValues = internalSearch.get(orgUnitIdValue);
236            _orgUnitRemoteIds.put(orgUnitIdValue, orgUnitValues.get(orgUnitRemoteIdColumnName).toString());
237            
238            Map<String, List<String>> mapping = getMapping();
239            if (mapping.containsKey(OrganisationChartPageHandler.PARENT_ORGUNIT_ATTRIBUTE_NAME))
240            {
241                // We take the first because it's no sense to defined two sql columns to define orgunit parents
242                String parentColumn = mapping.get(OrganisationChartPageHandler.PARENT_ORGUNIT_ATTRIBUTE_NAME).get(0); 
243                if (orgUnitValues.containsKey(parentColumn))
244                {
245                    String parentId = Optional.of(orgUnitValues)
246                            .map(v -> v.get(parentColumn))
247                            .map(Object::toString)
248                            .orElse(null); 
249                    _orgUnitParents.put(orgUnitIdValue, parentId);
250                }
251            }
252        }
253        
254        return internalSearch;
255    }
256    
257    @Override
258    public List<ModifiableDefaultContent> populate(Logger logger)
259    {
260        List<ModifiableDefaultContent> populateContents = super.populate(logger);
261        logger.info("{} contents have been modified to update the parent-child relations.", _orgUnitsToApplyChanges.size());
262        
263        return populateContents;
264    }
265    
266    @Override
267    public List<ModifiableDefaultContent> _internalPopulate(Logger logger)
268    {
269        _orgUnitParents = new HashMap<>();
270        _orgUnitRemoteIds = new HashMap<>();
271        _orgUnitUsers = _getOrgUnitUser(logger);
272        _orgUnitsToApplyChanges = new HashSet<>();
273        
274        List<ModifiableDefaultContent> contents = super._internalPopulate(logger);
275        
276        //All orgunits are imported, now we can set relation with parent orgunit
277        _setContentsRelationWithParentOrgunit(contents, logger);
278        
279        for (WorkflowAwareContent content : _orgUnitsToApplyChanges)
280        {
281            try
282            {
283                I18nizableText comment = new I18nizableText("plugin.user-directory", "PLUGINS_USER_DIRECTORY_WORKFLOW_ACTION_EDIT_ORGUNIT_REFERENCE_MSG");
284                _applyChanges(content, getSynchronizeActionId(), _i18nUtils.translate(comment, content.getLanguage()));
285            }
286            catch (WorkflowException e)
287            {
288                logger.error("Can't apply change to content : " + content.getId());
289            }
290        }
291        
292        return contents;
293    }
294    
295    @Override
296    protected List<ModifiableDefaultContent> _importOrSynchronizeContent(String idValue, String lang, Map<String, List<Object>> remoteValues, boolean forceImport, Logger logger)
297    {
298        remoteValues.remove(OrganisationChartPageHandler.PARENT_ORGUNIT_ATTRIBUTE_NAME);
299        
300        List<ModifiableDefaultContent> contents = super._importOrSynchronizeContent(idValue, lang, remoteValues, forceImport, logger);
301        for (ModifiableDefaultContent content : contents)
302        {
303            try
304            {
305                content.getNode().setProperty(ORGUNIT_REMOTE_ID_INTERNAL_METADATA, _orgUnitRemoteIds.get(idValue));
306            }
307            catch (RepositoryException e)
308            {
309                _nbError++;
310                logger.error("An error occurred while importing or synchronizing content", e);
311            }
312        }
313        
314        return contents;
315    }
316    
317    @Override
318    protected boolean additionalSynchronizeOperations(ModifiableDefaultContent content, Map<String, List<Object>> remoteValues, Logger logger)
319    {
320        boolean hasChanges = super.additionalSynchronizeOperations(content, remoteValues, logger);
321        return _additionalOperations(content, remoteValues, logger) || hasChanges;
322    }
323    
324    @Override
325    protected boolean additionalImportOperations(ModifiableDefaultContent content, Map<String, List<Object>> remoteValues, Map<String, Object> importParams, Logger logger)
326    {
327        boolean hasChanges = super.additionalImportOperations(content, remoteValues, importParams, logger);
328        return _additionalOperations(content, remoteValues, logger) || hasChanges;
329    }
330    
331    /**
332     * Do additional operations
333     * @param orgUnit the orgUnit content
334     * @param remoteValues the remote values
335     * @param logger the logger
336     * @return true if changes
337     */
338    protected boolean _additionalOperations(ModifiableDefaultContent orgUnit, Map<String, List<Object>> remoteValues, Logger logger)
339    {
340        boolean hasChanges = false;
341        
342        String orgUnitIdValue = _getIdFieldValue(orgUnit);
343        try
344        {
345            hasChanges = _synchronizeUserRepeaterOperation(orgUnit, orgUnitIdValue, logger);
346        }
347        catch (Exception e) 
348        {
349            logger.error("An error occurred synchronizing user repeater for orgunit " + orgUnit.getId(), e);
350        }
351        
352        return hasChanges;
353    }
354    
355    /**
356     * Get orgUnit user map
357     * @param logger the logger
358     * @return the orgUnit user map
359     */
360    protected Map<String, Map<String, String>> _getOrgUnitUser(Logger logger)
361    {
362        Map<String, Map<String, String>> orgUnitUsers = new HashMap<>();
363        
364        Map<String, List<String>> mapping = getMapping();
365        String idField = getIdField();
366        List<String> remoteOrgUnitKeys = mapping.get(idField);
367        if (remoteOrgUnitKeys != null && remoteOrgUnitKeys.size() > 0)
368        {
369            String remoteOrgUnitKey = remoteOrgUnitKeys.get(0);
370        
371            Map<String, Object> userParameters = _getSearchUserParameters(remoteOrgUnitKey, logger);
372            List<Map<String, Object>> searchUserList = _sqlUserDAO.searchUser(userParameters, getDataSourceId());
373            
374            String loginColumnName = getLoginUserColumnName();
375            String roleColumnName = getRoleUserColumnName();
376            
377            List<String> userColumns = new ArrayList<>();
378            userColumns.add(loginColumnName);
379            userColumns.add(remoteOrgUnitKey);
380            if (StringUtils.isNotBlank(roleColumnName))
381            {
382                userColumns.add(roleColumnName);
383            }
384
385            for (Map<String, Object> userMap : searchUserList)
386            {
387                Map<String, Object> normalizedUserMap = _getNormalizedSearchResult(userColumns, userMap);
388                _fillOrgUnitUserMap(orgUnitUsers, remoteOrgUnitKey, loginColumnName, roleColumnName, normalizedUserMap, logger);
389            }
390        }
391        
392        return orgUnitUsers;
393    }
394
395    /**
396     * Fill the orgUnitUsers map
397     * @param orgUnitUsers the orgunit user map
398     * @param remoteOrgUnitKey the remote key
399     * @param loginColumnName the login column name
400     * @param roleColumnName the role column name
401     * @param normalizedUserMap the normalized search user map
402     * @param logger the logger
403     */
404    protected void _fillOrgUnitUserMap(Map<String, Map<String, String>> orgUnitUsers, String remoteOrgUnitKey, String loginColumnName, String roleColumnName, Map<String, Object> normalizedUserMap, Logger logger)
405    {
406        String loginValue = (normalizedUserMap.get(loginColumnName) == null) ? null : String.valueOf(normalizedUserMap.get(loginColumnName)); 
407        String orgUnitIdValue = normalizedUserMap.get(remoteOrgUnitKey).toString();
408        
409        if (StringUtils.isNotBlank(loginValue))
410        {
411            String roleValue = null;
412            if (StringUtils.isNotBlank(roleColumnName))
413            {
414                roleValue = (normalizedUserMap.get(roleColumnName) == null) ? null :  String.valueOf(normalizedUserMap.get(roleColumnName));
415            }
416
417            if (!orgUnitUsers.containsKey(orgUnitIdValue))
418            {
419                orgUnitUsers.put(orgUnitIdValue, Maps.newLinkedHashMap());
420            }
421            
422            Map<String, String> orgUnitUserMap = orgUnitUsers.get(orgUnitIdValue);
423            orgUnitUserMap.put(loginValue, roleValue);
424        }
425        else
426        {
427            logger.warn("Can't add user to orgunit '" + orgUnitIdValue + "' because the login value is blank ...");
428        }
429    }
430
431    /**
432     * Set all orgunit parents relation for each synchronized content
433     * @param orgUnitContents the synchronized content
434     * @param logger the logger
435     */
436    protected void _setContentsRelationWithParentOrgunit(List<ModifiableDefaultContent> orgUnitContents, Logger logger)
437    {
438        for (ModifiableDefaultContent orgUnitContent : orgUnitContents)
439        {
440            String orgUnitIdValue = _getIdFieldValue(orgUnitContent);
441            String parentIdSQL = _orgUnitParents.get(orgUnitIdValue);
442            
443            String parentContentId = orgUnitContent.getMetadataHolder().getString(OrganisationChartPageHandler.PARENT_ORGUNIT_ATTRIBUTE_NAME, null);
444            Content newParentContent = _getOrgUnitContentFromRemoteId(parentIdSQL, orgUnitContent.getLanguage(), logger);
445            
446            if (StringUtils.isNotBlank(parentIdSQL) && newParentContent == null)
447            {
448                logger.warn("The orgUnit with sql id '" + parentIdSQL + "' doesn't exist. So this orgUnit is consider as null");
449            }
450            
451            // We set the relation if parentContent and newParentContent is not the same
452            if (!StringUtils.equals(
453                    // Old parent content ID
454                    Optional.ofNullable(parentContentId)
455                        .filter(StringUtils::isNotBlank)
456                        .orElse(null),
457                    // New parent content ID
458                    Optional.ofNullable(newParentContent)
459                        .map(AmetysObject::getId)
460                        .orElse(null))
461                )
462            {
463                try
464                {
465                    _setRelationWithParentOrgunit((ModifiableDefaultContent) newParentContent, orgUnitContent, logger);
466                }
467                catch (Exception e)
468                {
469                    _nbError++;
470                    String newParentId = newParentContent != null ? newParentContent.getId() : StringUtils.EMPTY;
471                    logger.error("Can't set parent relation between the parent '" + newParentId + "' and the child '" +  orgUnitContent.getId() + "'.", e);
472                }
473            }
474        }
475    }
476
477    /**
478     * Set the relation between the orgunit parent and its child
479     * @param parentContent the parent content
480     * @param childContent the child content
481     * @param logger the logger
482     * @throws WorkflowException if an error occurred
483     */
484    protected void _setRelationWithParentOrgunit(ModifiableDefaultContent parentContent, ModifiableDefaultContent childContent, Logger logger) throws WorkflowException
485    {
486        ModifiableCompositeMetadata childMetadataHolder = childContent.getMetadataHolder();
487        String parentOrgUnitId = childMetadataHolder.getString(OrganisationChartPageHandler.PARENT_ORGUNIT_ATTRIBUTE_NAME, null);
488
489        if (StringUtils.isNotBlank(parentOrgUnitId))
490        {
491            // We remove relation from old parent
492            ModifiableDefaultContent oldParentContent = _resolver.resolveById(parentOrgUnitId);
493            ModifiableCompositeMetadata oldParentMetadataHolder = oldParentContent.getMetadataHolder();
494            
495            String[] oldChildOrgUnit = oldParentMetadataHolder.getStringArray(OrganisationChartPageHandler.CHILD_ORGUNIT_ATTRIBUTE_NAME, ArrayUtils.EMPTY_STRING_ARRAY);
496            
497            String[] newValues = ArrayUtils.removeElement(oldChildOrgUnit, childContent.getId());
498            ExternalizableMetadataHelper.setMetadata(oldParentMetadataHolder, OrganisationChartPageHandler.CHILD_ORGUNIT_ATTRIBUTE_NAME, newValues);
499            
500            _orgUnitsToApplyChanges.add(oldParentContent);
501        }
502
503        if (parentContent == null)
504        {
505            // The parent content is null, so just remove parent metadata
506            childContent.getMetadataHolder().removeMetadata(OrganisationChartPageHandler.PARENT_ORGUNIT_ATTRIBUTE_NAME);
507            _orgUnitsToApplyChanges.add(childContent);
508        }
509        else
510        {        
511            // Change child orgunit metadata for parent content
512            ModifiableCompositeMetadata parentMetadataHolder = parentContent.getMetadataHolder();
513            String[] childOrgUnit = parentMetadataHolder.getStringArray(OrganisationChartPageHandler.CHILD_ORGUNIT_ATTRIBUTE_NAME, ArrayUtils.EMPTY_STRING_ARRAY);
514            
515            String[] newValues = ArrayUtils.add(childOrgUnit, childContent.getId());
516            ExternalizableMetadataHelper.setMetadata(parentMetadataHolder, OrganisationChartPageHandler.CHILD_ORGUNIT_ATTRIBUTE_NAME, newValues);
517            _orgUnitsToApplyChanges.add(parentContent);
518            
519            // Change parent orgunit metadata for child content
520            childMetadataHolder.setMetadata(OrganisationChartPageHandler.PARENT_ORGUNIT_ATTRIBUTE_NAME, parentContent.getId());
521            _orgUnitsToApplyChanges.add(childContent);
522        }
523    }
524    
525    private void _applyChanges(WorkflowAwareContent content, int actionId, String comment) throws WorkflowException
526    {
527        content.saveChanges();
528        ((DefaultAmetysObject) content).checkpoint();
529        
530        // Notify listeners
531        Map<String, Object> eventParams = new HashMap<>();
532        eventParams.put(ObservationConstants.ARGS_CONTENT, content);
533        eventParams.put(ObservationConstants.ARGS_CONTENT_ID, content.getId());
534        
535        _observationManager.notify(new Event(ObservationConstants.EVENT_CONTENT_MODIFIED, _userProvider.getUser(), eventParams));
536       
537        Map<String, Object> inputs = new HashMap<>();
538        inputs.put("comment", comment);
539        _contentWorkflowHelper.doAction(content, actionId, inputs);
540    }
541
542    /**
543     * Synchronize user repeater for orgunit content
544     * @param orgUnitContent the orgunit content
545     * @param orgUnitIdValue the orgUnit id value
546     * @param logger the logger
547     * @return true if changes
548     * @throws WorkflowException if an error occurred
549     */
550    protected boolean _synchronizeUserRepeaterOperation(ModifiableDefaultContent orgUnitContent, String orgUnitIdValue, Logger logger) throws WorkflowException
551    {
552        boolean hasChanges = false;
553        
554        Map<String, String> orgUnitUsers = _orgUnitUsers.get(orgUnitIdValue);
555        if (orgUnitUsers != null)
556        {
557            hasChanges = _synchronizeUserRepeater(orgUnitContent, orgUnitUsers, logger);
558        }
559        else if (orgUnitContent.hasValue(OrganisationChartPageHandler.ORGUNIT_USERS_ATTRIBUTE_NAME))
560        {
561            orgUnitContent.removeValue(OrganisationChartPageHandler.ORGUNIT_USERS_ATTRIBUTE_NAME);
562        }
563        
564        return hasChanges;
565    }
566
567    /**
568     * True if the user repeater needs changes
569     * @param orgUnitContent the orgUnit content
570     * @param orgUnitUsers the map of orgunit users
571     * @param logger the logger
572     * @return true if the user repeater needs changes
573     * @throws WorkflowException if an error occurred
574     */
575    protected boolean _synchronizeUserRepeater(ModifiableDefaultContent orgUnitContent, Map<String, String> orgUnitUsers, Logger logger) throws WorkflowException
576    {
577        boolean hasChanges = false;
578        
579        Map<Integer, ModifiableModelAwareRepeaterEntry> repeaterEntriesPosition = new HashMap<>();
580        ModifiableModelAwareRepeater userRepeater = orgUnitContent.getRepeater(OrganisationChartPageHandler.ORGUNIT_USERS_ATTRIBUTE_NAME, true);
581        List<String> importedUsers = new ArrayList<>();
582
583        hasChanges = _synchronizedRepeaterEntries(orgUnitContent, userRepeater, repeaterEntriesPosition, orgUnitUsers, importedUsers, logger) || hasChanges;
584        hasChanges = _addNewEntries(orgUnitContent, userRepeater, repeaterEntriesPosition, orgUnitUsers, importedUsers, logger) || hasChanges;
585        hasChanges = _reorderEntries(userRepeater, repeaterEntriesPosition, logger) || hasChanges;
586        
587        return hasChanges;
588    }
589
590    /**
591     * Synchronize entries already imported
592     * @param orgUnitContent the orgUnit content
593     * @param userRepeater the user repeater
594     * @param repeaterEntriesPosition the entries with the new positions
595     * @param orgUnitUsers the orgUnit users
596     * @param importedUsers the user login already imported in the repeater
597     * @param logger the logger
598     * @return true if new entries are added
599     * @throws WorkflowException if an error occurred
600     */
601    protected boolean _synchronizedRepeaterEntries(ModifiableDefaultContent orgUnitContent, ModifiableModelAwareRepeater userRepeater, Map<Integer, ModifiableModelAwareRepeaterEntry> repeaterEntriesPosition, Map<String, String> orgUnitUsers, List<String> importedUsers, Logger logger) throws WorkflowException
602    {
603        boolean hasChanges = false;
604        
605        String roleUserColumnName = getRoleUserColumnName();
606        List userIds = new ArrayList<>(orgUnitUsers.keySet());
607        List< ? extends ModifiableModelAwareRepeaterEntry> entries = userRepeater.getEntries();
608        
609        // We need to reverse the entries because when we remove an entry and shift next entries, we don't disrupt the loop.
610        List<ModifiableModelAwareRepeaterEntry> entriesToReverse = new ArrayList<>(entries);
611        Collections.reverse(entriesToReverse);
612
613        // Compare with existing user entries
614        for (ModifiableModelAwareRepeaterEntry entry : entriesToReverse)
615        {
616            ContentValue userValue = entry.getValue(OrganisationChartPageHandler.ORGUNIT_USER_ATTRIBUTE_NAME);
617            String role = entry.getValue(OrganisationChartPageHandler.ORGUNIT_USER_ROLE_ATTRIBUTE_NAME);
618            
619            if (userValue != null)
620            {
621                try
622                {
623                    Content userContent = userValue.getContent();
624                    String userIdValue = _getUserIdValue(userContent, logger);
625                    if (!orgUnitUsers.containsKey(userIdValue))
626                    {
627                        userRepeater.removeEntry(entry.getPosition());
628                        _removeRelationForUser(orgUnitContent, userContent);
629                        hasChanges = true;
630                    }
631                    else 
632                    {
633                        repeaterEntriesPosition.put(userIds.indexOf(userIdValue) + 1, entry);
634                        
635                        String newRole = orgUnitUsers.get(userIdValue);
636                        if (StringUtils.isNotBlank(roleUserColumnName) && !StringUtils.equals(role, newRole))
637                        {
638                            // The user id belong to the user to synchronize and its role has changed
639                            if (StringUtils.isNotBlank(newRole))
640                            {
641                                entry.setValue(OrganisationChartPageHandler.ORGUNIT_USER_ROLE_ATTRIBUTE_NAME, newRole);
642                                hasChanges = true;
643                            }
644                            else if (entry.hasValue(OrganisationChartPageHandler.ORGUNIT_USER_ROLE_ATTRIBUTE_NAME))
645                            {
646                                entry.removeValue(OrganisationChartPageHandler.ORGUNIT_USER_ROLE_ATTRIBUTE_NAME);
647                                hasChanges = true;
648                            }
649                        }
650                        
651                        // Fix relation between orgUnit and users. If the users not contain the orgunit, set the relation between them
652                        hasChanges = _setInvertRelationForUser(orgUnitContent, userContent) || hasChanges;
653                    }
654                    
655                    importedUsers.add(userIdValue);
656                }
657                catch (UnknownAmetysObjectException e) 
658                {
659                    // The user is not found
660                    logger.warn("Can't find the content user in the repeater with id '" + userValue.getContentId() + "'. So the entry will be removed", e);
661                    userRepeater.removeEntry(entry.getPosition());
662                    hasChanges = true;
663                }
664            }
665            else // no user id
666            {
667                logger.warn("The user content is no defined in the repeater for orgUnit id '" + orgUnitContent.getId() + "'. So the entry will be removed");
668                userRepeater.removeEntry(entry.getPosition());
669                hasChanges = true;
670            }
671        }
672        return hasChanges;
673    }
674
675    /**
676     * Add the new entries
677     * @param orgUnitContent the orgUnit content
678     * @param userRepeater the user repeater
679     * @param repeaterEntriesPosition the entries with the new positions
680     * @param orgUnitUsers the orgUnit users
681     * @param importedUsers the user login already imported in the repeater
682     * @param logger the logger
683     * @return true if new entries are added
684     * @throws WorkflowException if an error occurred
685     */
686    protected boolean _addNewEntries(ModifiableDefaultContent orgUnitContent, ModifiableModelAwareRepeater userRepeater, Map<Integer, ModifiableModelAwareRepeaterEntry> repeaterEntriesPosition, Map<String, String> orgUnitUsers, List<String> importedUsers,  Logger logger) throws WorkflowException
687    {
688        boolean hasChanges = false;
689        
690        List userIds = new ArrayList<>(orgUnitUsers.keySet());
691        String lang = orgUnitContent.getLanguage();
692        for (String login : orgUnitUsers.keySet())
693        {
694            if (!importedUsers.contains(login))
695            {
696                Content userContent = _getUserContent(login, lang, logger);
697                if (userContent != null)
698                {
699                    ModifiableModelAwareRepeaterEntry newEntry = userRepeater.addEntry();
700                    newEntry.setValue(OrganisationChartPageHandler.ORGUNIT_USER_ATTRIBUTE_NAME, userContent.getId());
701                    
702                    _setInvertRelationForUser(orgUnitContent, userContent);
703                    
704                    String role = orgUnitUsers.get(login);
705                    if (StringUtils.isNotBlank(role))
706                    {
707                        newEntry.setValue(OrganisationChartPageHandler.ORGUNIT_USER_ROLE_ATTRIBUTE_NAME, role);
708                    }
709                    
710                    repeaterEntriesPosition.put(userIds.indexOf(login) + 1, newEntry);
711                    hasChanges = true;
712                }
713                else
714                {
715                    logger.warn("Can't add user '" + login + "' to orgunit '" + orgUnitContent.getTitle() + "' because he doesn't exist"); 
716                }
717            }
718        }
719        return hasChanges;
720    }
721
722    /**
723     * Reorder repeater entries if needed
724     * @param userRepeater the user repeater
725     * @param repeaterEntriesPosition the repeater entries with the new positions
726     * @param logger the logger
727     * @return true if the entries are reordered
728     */
729    protected boolean _reorderEntries(ModifiableModelAwareRepeater userRepeater, Map<Integer, ModifiableModelAwareRepeaterEntry> repeaterEntriesPosition, Logger logger)
730    {
731        if (StringUtils.isNotBlank(getUserSortColumnName()) && isUserSortPrevail())
732        {
733            Map<Integer, Integer> positionsMapping = new HashMap<>();
734            int nextPosition = 1;
735            for (int newPosition = 1; newPosition <= repeaterEntriesPosition.size(); newPosition++)
736            {
737                nextPosition = _getNextPositionEntry(nextPosition, repeaterEntriesPosition);
738                
739                ModifiableModelAwareRepeaterEntry entry = repeaterEntriesPosition.get(nextPosition);
740                Integer oldPosition = entry.getPosition();
741                positionsMapping.put(oldPosition, newPosition);
742                
743                nextPosition++;
744            }
745            
746            return userRepeater.moveEntries(positionsMapping, positionsMapping.size());
747        }
748        
749        return false;
750    }
751    
752    private int _getNextPositionEntry(int position, Map<Integer, ModifiableModelAwareRepeaterEntry> repeaterEntriesPosition)
753    {
754        int maxPosition = Collections.max(repeaterEntriesPosition.keySet());
755        
756        int entryId = position;
757        while (!repeaterEntriesPosition.containsKey(entryId) && entryId <= maxPosition)
758        {
759            entryId++;
760        }
761
762        return entryId;
763    }
764
765    /**
766     * Set invert relation for user
767     * @param orgUnitContent the orgUnit content
768     * @param userContent the user content
769     * @return true if changes
770     * @throws WorkflowException if an error occurred
771     */
772    protected boolean _setInvertRelationForUser(ModifiableDefaultContent orgUnitContent, Content userContent) throws WorkflowException
773    {
774        boolean hasChanges = false;
775        
776        String orgUnitId = orgUnitContent.getId();
777        ContentValue[] values = userContent.getValue(UserDirectoryHelper.ORGUNITS_ATTRIBUTE, false, new ContentValue[0]);
778        boolean alreadyPresent = Stream.of(values)
779                .map(ContentValue::getContentId)
780                .filter(orgUnitId::equals)
781                .findAny()
782                .isPresent();
783        
784        if (!alreadyPresent)
785        {
786            ContentValue[] newValues = ArrayUtils.add(values, new ContentValue(_resolver, orgUnitId));
787            ((ModifiableContent) userContent).setValue(UserDirectoryHelper.ORGUNITS_ATTRIBUTE, newValues);
788            hasChanges = true;
789            
790            I18nizableText comment = new I18nizableText("plugin.user-directory", "PLUGINS_USER_DIRECTORY_WORKFLOW_ACTION_EDIT_USER_REFERENCE_MSG");
791            _applyChanges((WorkflowAwareContent) userContent, getSynchronizeActionId(), _i18nUtils.translate(comment, userContent.getLanguage()));
792        }
793        
794        return hasChanges;
795    }
796    
797    /**
798     * Remove relation for user
799     * @param orgUnitContent the orgUnit content
800     * @param userContent the user content
801     * @return true if changes
802     * @throws WorkflowException if an error occurred
803     */
804    protected boolean _removeRelationForUser(ModifiableDefaultContent orgUnitContent, Content userContent) throws WorkflowException
805    {
806        boolean hasChanges = false;
807        
808        String orgUnitId = orgUnitContent.getId();
809        ContentValue[] values = userContent.getValue(UserDirectoryHelper.ORGUNITS_ATTRIBUTE, false, new ContentValue[0]);
810        boolean alreadyPresent = Stream.of(values)
811                .map(ContentValue::getContentId)
812                .filter(orgUnitId::equals)
813                .findAny()
814                .isPresent();
815        
816        if (alreadyPresent)
817        {
818            ContentValue[] newValues = ArrayUtils.removeElement(values, new ContentValue(_resolver, orgUnitId));
819            ((ModifiableContent) userContent).setValue(UserDirectoryHelper.ORGUNITS_ATTRIBUTE, newValues);
820            hasChanges = true;
821            
822            I18nizableText comment = new I18nizableText("plugin.user-directory", "PLUGINS_USER_DIRECTORY_WORKFLOW_ACTION_EDIT_USER_REFERENCE_MSG");
823            _applyChanges((WorkflowAwareContent) userContent, getSynchronizeActionId(), _i18nUtils.translate(comment, userContent.getLanguage()));
824        }
825        
826        return hasChanges;
827    }
828    
829    /**
830     * Get user content from login value
831     * @param loginValue the login value
832     * @param lang the language
833     * @param logger the logger
834     * @return the user content
835     */
836    protected Content _getUserContent(String loginValue, String lang, Logger logger)
837    {
838        String loginMetadata = getLoginUserMetadataName();
839        Set<String> contentTypes = _contentTypeEP.getSubTypes(UserDirectoryPageHandler.ABSTRACT_USER_CONTENT_TYPE);
840        
841        Expression ctypeExpression = new ContentTypeExpression(Operator.EQ, contentTypes.toArray(new String[contentTypes.size()]));
842        LanguageExpression languageExpression = new LanguageExpression(Operator.EQ, lang);
843        StringExpression metadataExp = new StringExpression(loginMetadata, Operator.EQ, loginValue);
844        
845        Expression userExp = new AndExpression(metadataExp, ctypeExpression, languageExpression);
846        String xPathQuery = ContentQueryHelper.getContentXPathQuery(userExp);
847        
848        AmetysObjectIterable<Content> contentQuery = _resolver.query(xPathQuery);
849        AmetysObjectIterator<Content> contentIterator = contentQuery.iterator();
850        if (contentIterator.hasNext())
851        {
852            return contentIterator.next();
853        }
854        
855        return null;
856    }
857    
858    /**
859     * Get orgunit content from the remote Id
860     * @param remoteId the remote Id
861     * @param lang the language
862     * @param logger the logger
863     * @return the orgunit content
864     */
865    protected Content _getOrgUnitContentFromRemoteId(String remoteId, String lang, Logger logger)
866    {
867        Expression ctypeExpression = new ContentTypeExpression(Operator.EQ, OrganisationChartPageHandler.ORGUNIT_CONTENT_TYPE);
868        RemoteIdOrgunitExpression remoteIdOrgunitExpression = new RemoteIdOrgunitExpression(remoteId);
869        LanguageExpression languageExpression = new LanguageExpression(Operator.EQ, lang);
870        
871        CollectionExpression collectionExpression = new CollectionExpression(getId());
872        
873        Expression userExp = new AndExpression(remoteIdOrgunitExpression, ctypeExpression, languageExpression, collectionExpression);
874        String xPathQuery = ContentQueryHelper.getContentXPathQuery(userExp);
875        
876        AmetysObjectIterable<Content> contentQuery = _resolver.query(xPathQuery);
877        AmetysObjectIterator<Content> contentIterator = contentQuery.iterator();
878        if (contentIterator.hasNext())
879        {
880            return contentIterator.next();
881        }
882        
883        return null;
884    }
885    
886    /**
887     * Get the parameters map for user mybatis search
888     * @param orgUnitColumnKey the column name of the orgunit key
889     * @param logger the logger
890     * @return the parameter map
891     */
892    protected Map<String, Object> _getSearchUserParameters(String orgUnitColumnKey, Logger logger)
893    {
894        Map<String, Object> params = new HashMap<>();
895        params.put("loginColumnName", getLoginUserColumnName());
896        params.put("tableUser", getUserTableName());
897        params.put("tableOrgUnit", getTableName());
898        params.put("joinColumnName", getOrgunitJoinColumnNameForUser());
899        params.put("orgUnitColumnKey", orgUnitColumnKey);
900        params.put("orgUnitIdColumnName", getOrgUnitRemoteIdColumnName());
901        
902        String roleUserColumnName = getRoleUserColumnName();
903        if (StringUtils.isNotBlank(roleUserColumnName))
904        {
905            params.put("roleColumnName", roleUserColumnName);
906        }
907        
908        String sortColumnName = getUserSortColumnName();
909        if (StringUtils.isNotBlank(sortColumnName))
910        {
911            params.put("sortColumnName", sortColumnName);
912        }
913        
914
915        return params;
916    }
917    
918    /**
919     * Get the user id value
920     * @param userContent the user content
921     * @param logger the logger
922     * @return the user id value
923     */
924    protected String _getUserIdValue(Content userContent, Logger logger)
925    {
926        String loginMetadataName = getLoginUserMetadataName();
927        return userContent.getValue(loginMetadataName);
928    }
929    
930    @Override
931    protected int _deleteContents(List<Content> contentsToRemove, Logger logger)
932    {
933        return _deleteOrgUnitComponent.deleteContentsWithLog(contentsToRemove, MapUtils.EMPTY_SORTED_MAP, MapUtils.EMPTY_SORTED_MAP, logger);
934    }
935}