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