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