001/*
002 *  Copyright 2018 Anyware Services
003 *
004 *  Licensed under the Apache License, Version 2.0 (the "License");
005 *  you may not use this file except in compliance with the License.
006 *  You may obtain a copy of the License at
007 *
008 *      http://www.apache.org/licenses/LICENSE-2.0
009 *
010 *  Unless required by applicable law or agreed to in writing, software
011 *  distributed under the License is distributed on an "AS IS" BASIS,
012 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 *  See the License for the specific language governing permissions and
014 *  limitations under the License.
015 */
016package org.ametys.plugins.userdirectory.synchronize;
017
018import java.util.ArrayList;
019import java.util.HashMap;
020import java.util.HashSet;
021import java.util.List;
022import java.util.Map;
023import java.util.Set;
024import java.util.stream.Collectors;
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.OutgoingReferences;
038import org.ametys.cms.content.references.OutgoingReferencesExtractor;
039import org.ametys.cms.repository.Content;
040import org.ametys.cms.repository.ContentQueryHelper;
041import org.ametys.cms.repository.ContentTypeExpression;
042import org.ametys.cms.repository.LanguageExpression;
043import org.ametys.cms.repository.ModifiableContent;
044import org.ametys.cms.repository.ModifiableDefaultContent;
045import org.ametys.cms.repository.WorkflowAwareContent;
046import org.ametys.cms.workflow.ContentWorkflowHelper;
047import org.ametys.core.observation.Event;
048import org.ametys.core.user.CurrentUserProvider;
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.UserDirectoryPageHandler;
063import org.ametys.plugins.userdirectory.expression.RemoteIdOrgunitExpression;
064
065import com.google.common.collect.Maps;
066import com.opensymphony.workflow.WorkflowException;
067
068/**
069 * Synchronizable collection for UD Orgunits
070 */
071public class SQLSynchronizableUDOrgunitCollection extends SQLSynchronizableContentsCollection
072{
073    /** The metadata name for orgUnit users repeater */
074    public static final String ORGUNIT_USERS_METADATA = "users";
075    /** The metadata name for orgUnit user in repeater */
076    public static final String ORGUNIT_USER_METADATA = "user";
077    /** The metadata name for orgUnit user role in repeater */
078    public static final String ORGUNIT_USER_ROLE_METADATA = "role";
079    
080    /** The internal metadata name for orgUnit remote sql id */
081    public static final String ORGUNIT_REMOTE_ID_INTERNAL_METADATA = RepositoryConstants.NAMESPACE_PREFIX_INTERNAL + ":orgunit-remote-id";
082
083    private static final String __PARAM_SQL_TABLE_USER = "tableNameUser";
084    private static final String __PARAM_SQL_ORGUNIT_JOIN_COLUMN_NAME = "orgUnitJoinColumnName";
085    private static final String __PARAM_LOGIN_USER_METADATA_NAME = "loginUser";
086    private static final String __PARAM_SQL_LOGIN_USER_COLUMN_NAME = "loginColumnName";
087    private static final String __PARAM_SQL_ROLE_USER_COLUMN_NAME = "roleColumnName";
088    private static final String __PARAM_SQL_ORGUNIT_REMOTE_ID_COLUMN_NAME = "orgunitRemoteIdColumnName";
089    
090    /** The map which link orgunit with this parent */
091    protected Map<String, String> _orgUnitParents;
092    
093    /** The map which link orgunit with users (userId and role) */
094    protected Map<String, Map<String, String>> _orgUnitUsers;
095    
096    /** The map which link orgunit with sql remote ids */
097    protected Map<String, String> _orgUnitRemoteIds;
098    
099    /** The set of orgunits to apply changes */
100    protected Set<WorkflowAwareContent> _orgUnitsToApplyChanges;
101    
102    /** The sql user search DAO */
103    protected SQLUserSearchDAO _sqlUserDAO;
104    
105    /** The organisation chart page handler */
106    protected OrganisationChartPageHandler _orgChartPageHandler;
107    
108    /** The content workflow helper */
109    protected ContentWorkflowHelper _contentWorkflowHelper;
110    
111    /** The current user provider */
112    protected CurrentUserProvider _userProvider;
113    
114    /** The OutgoingReferences extractor */
115    protected OutgoingReferencesExtractor _outgoingReferencesExtractor;
116    
117    /** The delete orgUnit component */
118    protected DeleteOrgUnitComponent _deleteOrgUnitComponent;
119    
120    @Override
121    public void service(ServiceManager smanager) throws ServiceException
122    {
123        super.service(smanager);
124        _sqlUserDAO = (SQLUserSearchDAO) smanager.lookup(SQLUserSearchDAO.ROLE);
125        _orgChartPageHandler = (OrganisationChartPageHandler) smanager.lookup(OrganisationChartPageHandler.ROLE);
126        _contentWorkflowHelper = (ContentWorkflowHelper) smanager.lookup(ContentWorkflowHelper.ROLE);
127        _userProvider = (CurrentUserProvider) smanager.lookup(CurrentUserProvider.ROLE);
128        _outgoingReferencesExtractor = (OutgoingReferencesExtractor) smanager.lookup(OutgoingReferencesExtractor.ROLE);
129        _deleteOrgUnitComponent = (DeleteOrgUnitComponent) smanager.lookup(DeleteOrgUnitComponent.ROLE);
130    }
131    
132    /**
133     * Get the name of user SQL table
134     * @return The user SQL table name
135     */
136    public String getUserTableName()
137    {
138        return (String) getParameterValues().get(__PARAM_SQL_TABLE_USER);
139    }
140    
141    /**
142     * Get the name of the orgunit join column name of the user table 
143     * @return The name of the orgunit join column name
144     */
145    public String getOrgunitJoinColumnNameForUser()
146    {
147        return (String) getParameterValues().get(__PARAM_SQL_ORGUNIT_JOIN_COLUMN_NAME);
148    }
149    
150    /**
151     * Get the login user metadata name
152     * @return The login user metadata name
153     */
154    public String getLoginUserMetadataName()
155    {
156        return (String) getParameterValues().get(__PARAM_LOGIN_USER_METADATA_NAME);
157    }
158    
159    /**
160     * Get the login user column name
161     * @return The login user column name
162     */
163    public String getLoginUserColumnName()
164    {
165        return (String) getParameterValues().get(__PARAM_SQL_LOGIN_USER_COLUMN_NAME);
166    }
167    
168    /**
169     * Get the role user column name
170     * @return The role user column name
171     */
172    public String getRoleUserColumnName()
173    {
174        return (String) getParameterValues().get(__PARAM_SQL_ROLE_USER_COLUMN_NAME);
175    }
176    
177    /**
178     * Get the orgunit remote id column name
179     * @return The orgunit remote id column name
180     */
181    public String getOrgUnitRemoteIdColumnName()
182    {
183        return (String) getParameterValues().get(__PARAM_SQL_ORGUNIT_REMOTE_ID_COLUMN_NAME);
184    }
185    
186    @Override
187    protected Map<String, Object> _getSearchParameters(Map<String, Object> parameters, int offset, int limit, List<Object> sort, List<String> columns)
188    {
189        // Add the sql column name for the orgunit id. 
190        // It's for setting the ametys-internal:orgunit-remote-id and retrieve easily the orgUnit content with this Id 
191        String orgUnitIdColumnName = getOrgUnitRemoteIdColumnName();
192        if (!columns.contains(orgUnitIdColumnName))
193        {
194            columns.add(orgUnitIdColumnName);
195        }
196        
197        return super._getSearchParameters(parameters, offset, limit, sort, columns);
198    }
199    
200    @Override
201    protected Map<String, Map<String, Object>> internalSearch(Map<String, Object> parameters, int offset, int limit, List<Object> sort, Logger logger)
202    {
203        Map<String, Map<String, Object>> internalSearch = super.internalSearch(parameters, offset, limit, sort, logger);
204
205        // Fill _orgUnitRemoteIds and _orgUnitParents maps with search results
206        String orgUnitRemoteIdColumnName = getOrgUnitRemoteIdColumnName();
207        for (String orgUnitIdValue : internalSearch.keySet())
208        {
209            Map<String, Object> orgUnitValues = internalSearch.get(orgUnitIdValue);
210            _orgUnitRemoteIds.put(orgUnitIdValue, orgUnitValues.get(orgUnitRemoteIdColumnName).toString());
211            
212            Map<String, List<String>> mapping = getMapping();
213            if (mapping.containsKey(OrganisationChartPageHandler.METADATA_PARENT_ORGUNIT))
214            {
215                // We take the first because it's no sense to defined two sql columns to define orgunit parents
216                String parentColumn = mapping.get(OrganisationChartPageHandler.METADATA_PARENT_ORGUNIT).get(0); 
217                if (orgUnitValues.containsKey(parentColumn) && StringUtils.isNotBlank(orgUnitValues.get(parentColumn).toString()))
218                {
219                    _orgUnitParents.put(orgUnitIdValue, orgUnitValues.get(parentColumn).toString());
220                }
221            }
222        }
223        
224        return internalSearch;
225    }
226    
227    @Override
228    public List<ModifiableDefaultContent> _internalPopulate(Logger logger)
229    {
230        _orgUnitParents = new HashMap<>();
231        _orgUnitRemoteIds = new HashMap<>();
232        _orgUnitUsers = _getOrgUnitUser(logger);
233        _orgUnitsToApplyChanges = new HashSet<>();
234        
235        List<ModifiableDefaultContent> contents = super._internalPopulate(logger);
236        
237        //All orgunits are imported, now we can set relation with parent orgunit
238        _setContentsRelationWithParentOrgunit(contents, logger);
239        
240        for (WorkflowAwareContent content : _orgUnitsToApplyChanges)
241        {
242            try
243            {
244                _applyChanges(content);
245            }
246            catch (WorkflowException e)
247            {
248                logger.error("Can't apply change to content : " + content.getId());
249            }
250        }
251        
252        return contents;
253    }
254    
255    @Override
256    protected List<ModifiableDefaultContent> _importOrSynchronizeContent(String idValue, String lang, Map<String, List<Object>> remoteValues, boolean forceImport, Logger logger)
257    {
258        remoteValues.remove(OrganisationChartPageHandler.METADATA_PARENT_ORGUNIT);
259        
260        List<ModifiableDefaultContent> contents = super._importOrSynchronizeContent(idValue, lang, remoteValues, forceImport, logger);
261        for (ModifiableDefaultContent content : contents)
262        {
263            try
264            {
265                content.getNode().setProperty(ORGUNIT_REMOTE_ID_INTERNAL_METADATA, _orgUnitRemoteIds.get(idValue));
266            }
267            catch (RepositoryException e)
268            {
269                _nbError++;
270                logger.error("An error occurred while importing or synchronizing content", e);
271            }
272        }
273        
274        return contents;
275    }
276    
277    @Override
278    protected boolean additionalSynchronizeOperations(ModifiableDefaultContent content, Map<String, List<Object>> remoteValues, Logger logger)
279    {
280        boolean hasChanges = super.additionalSynchronizeOperations(content, remoteValues, logger);
281        return _additionalOperations(content, remoteValues, logger) || hasChanges;
282    }
283    
284    @Override
285    protected boolean additionalImportOperations(ModifiableDefaultContent content, Map<String, List<Object>> remoteValues, Map<String, Object> importParams, Logger logger)
286    {
287        boolean hasChanges = super.additionalImportOperations(content, remoteValues, importParams, logger);
288        return _additionalOperations(content, remoteValues, logger) || hasChanges;
289    }
290    
291    /**
292     * Do additional operations
293     * @param orgUnit the orgUnit content
294     * @param remoteValues the remote values
295     * @param logger the logger
296     * @return true if changes
297     */
298    protected boolean _additionalOperations(ModifiableDefaultContent orgUnit, Map<String, List<Object>> remoteValues, Logger logger)
299    {
300        boolean hasChanges = false;
301        
302        String orgUnitIdValue = _getIdFieldValue(orgUnit);
303        hasChanges = _synchronizeUserRepeaterOperation(orgUnit, orgUnitIdValue, logger);
304        
305        return hasChanges;
306    }
307    
308    /**
309     * Get orgUnit user map
310     * @param logger the logger
311     * @return the orgUnit user map
312     */
313    protected Map<String, Map<String, String>> _getOrgUnitUser(Logger logger)
314    {
315        Map<String, Map<String, String>> orgUnitUsers = new HashMap<>();
316        
317        Map<String, List<String>> mapping = getMapping();
318        String idField = getIdField();
319        List<String> remoteOrgUnitKeys = mapping.get(idField);
320        if (remoteOrgUnitKeys != null && remoteOrgUnitKeys.size() > 0)
321        {
322            String remoteOrgUnitKey = remoteOrgUnitKeys.get(0);
323        
324            Map<String, Object> userParameters = _getSearchUserParameters(remoteOrgUnitKey, logger);
325            List<Map<String, Object>> searchUserList = _sqlUserDAO.searchUser(userParameters, getDataSourceId());
326            
327            String loginColumnName = getLoginUserColumnName();
328            String roleColumnName = getRoleUserColumnName();
329            
330            List<String> userColumns = new ArrayList<>();
331            userColumns.add(loginColumnName);
332            userColumns.add(remoteOrgUnitKey);
333            if (StringUtils.isNotBlank(roleColumnName))
334            {
335                userColumns.add(roleColumnName);
336            }
337
338            for (Map<String, Object> userMap : searchUserList)
339            {
340                Map<String, Object> normalizedUserMap = _getNormalizedSearchResult(userColumns, userMap);
341                _fillOrgUnitUserMap(orgUnitUsers, remoteOrgUnitKey, loginColumnName, roleColumnName, normalizedUserMap, logger);
342            }
343        }
344        
345        return orgUnitUsers;
346    }
347
348    /**
349     * Fill the orgUnitUsers map
350     * @param orgUnitUsers the orgunit user map
351     * @param remoteOrgUnitKey the remote key
352     * @param loginColumnName the login column name
353     * @param roleColumnName the role column name
354     * @param normalizedUserMap the normalized search user map
355     * @param logger the logger
356     */
357    protected void _fillOrgUnitUserMap(Map<String, Map<String, String>> orgUnitUsers, String remoteOrgUnitKey, String loginColumnName, String roleColumnName, Map<String, Object> normalizedUserMap, Logger logger)
358    {
359        String loginValue = (normalizedUserMap.get(loginColumnName) == null) ? null : String.valueOf(normalizedUserMap.get(loginColumnName)); 
360        String orgUnitIdValue = normalizedUserMap.get(remoteOrgUnitKey).toString();
361        
362        if (StringUtils.isNotBlank(loginValue))
363        {
364            String roleValue = null;
365            if (StringUtils.isNotBlank(roleColumnName))
366            {
367                roleValue = (normalizedUserMap.get(roleColumnName) == null) ? null :  String.valueOf(normalizedUserMap.get(roleColumnName));
368            }
369
370            if (!orgUnitUsers.containsKey(orgUnitIdValue))
371            {
372                orgUnitUsers.put(orgUnitIdValue, Maps.newHashMap());
373            }
374            
375            Map<String, String> orgUnitUserMap = orgUnitUsers.get(orgUnitIdValue);
376            orgUnitUserMap.put(loginValue, roleValue);
377        }
378        else
379        {
380            logger.warn("Can't add user to orgunit '" + orgUnitIdValue + "' because the login value is blank ...");
381        }
382    }
383
384    /**
385     * Set all orgunit parents relation for each synchronized content
386     * @param orgUnitContents the synchronized content
387     * @param logger the logger
388     */
389    protected void _setContentsRelationWithParentOrgunit(List<ModifiableDefaultContent> orgUnitContents, Logger logger)
390    {
391        for (ModifiableDefaultContent orgUnitContent : orgUnitContents)
392        {
393            String orgUnitIdValue = _getIdFieldValue(orgUnitContent);
394            String parentIdSQL = _orgUnitParents.get(orgUnitIdValue);
395            
396            Content parentContent = _orgChartPageHandler.getParentContent(orgUnitContent);
397            Content newParentContent = _getOrgUnitContentFromRemoteId(parentIdSQL, orgUnitContent.getLanguage(), logger);
398            
399            if (StringUtils.isNotBlank(parentIdSQL) && newParentContent == null)
400            {
401                logger.warn("The orgUnit with sql id '{0}' doesn't exist. So this orgUnit is consider as null", parentIdSQL);
402            }
403            
404            //We set the relation if parentContent and newParentContent is not the same
405            if (!(parentContent == null && newParentContent == null
406                || (parentContent != null && newParentContent != null && StringUtils.equals(parentContent.getId(), newParentContent.getId()))))
407            {
408                try
409                {
410                    _setRelationWithParentOrgunit((ModifiableDefaultContent) newParentContent, orgUnitContent, logger);
411                }
412                catch (Exception e)
413                {
414                    _nbError++;
415                    String newParentId = newParentContent != null ? newParentContent.getId() : StringUtils.EMPTY;
416                    logger.error("Can't set parent relation between the parent '" + newParentId + "' and the child '" +  orgUnitContent.getId() + "'.", e);
417                }
418            }
419        }
420    }
421
422    /**
423     * Set the relation between the orgunit parent and its child
424     * @param parentContent the parent content
425     * @param childContent the child content
426     * @param logger the logger
427     * @throws WorkflowException if an error occurred
428     */
429    protected void _setRelationWithParentOrgunit(ModifiableDefaultContent parentContent, ModifiableDefaultContent childContent, Logger logger) throws WorkflowException
430    {
431        ModifiableCompositeMetadata childMetadataHolder = childContent.getMetadataHolder();
432        String parentOrgUnitId = childMetadataHolder.getString(OrganisationChartPageHandler.METADATA_PARENT_ORGUNIT, null);
433
434        if (StringUtils.isNotBlank(parentOrgUnitId))
435        {
436            // We remove relation from old parent
437            ModifiableDefaultContent oldParentContent = _resolver.resolveById(parentOrgUnitId);
438            ModifiableCompositeMetadata oldParentMetadataHolder = oldParentContent.getMetadataHolder();
439            
440            String[] oldChildOrgUnit = oldParentMetadataHolder.getStringArray(OrganisationChartPageHandler.METADATA_CHILD_ORGUNIT, ArrayUtils.EMPTY_STRING_ARRAY);
441            
442            String[] newValues = ArrayUtils.removeElement(oldChildOrgUnit, childContent.getId());
443            ExternalizableMetadataHelper.setMetadata(oldParentMetadataHolder, OrganisationChartPageHandler.METADATA_CHILD_ORGUNIT, newValues);
444            
445            _orgUnitsToApplyChanges.add(oldParentContent);
446        }
447
448        if (parentContent == null)
449        {
450            // The parent content is null, so just remove parent metadata
451            childContent.getMetadataHolder().removeMetadata(OrganisationChartPageHandler.METADATA_PARENT_ORGUNIT);
452            _orgUnitsToApplyChanges.add(childContent);
453        }
454        else
455        {        
456            // Change child orgunit metadata for parent content
457            ModifiableCompositeMetadata parentMetadataHolder = parentContent.getMetadataHolder();
458            String[] childOrgUnit = parentMetadataHolder.getStringArray(OrganisationChartPageHandler.METADATA_CHILD_ORGUNIT, ArrayUtils.EMPTY_STRING_ARRAY);
459            
460            String[] newValues = ArrayUtils.add(childOrgUnit, childContent.getId());
461            ExternalizableMetadataHelper.setMetadata(parentMetadataHolder, OrganisationChartPageHandler.METADATA_CHILD_ORGUNIT, newValues);
462            _orgUnitsToApplyChanges.add(parentContent);
463               
464            // Change parent orgunit metadata for child content
465            childMetadataHolder.setMetadata(OrganisationChartPageHandler.METADATA_PARENT_ORGUNIT, parentContent.getId());
466            _orgUnitsToApplyChanges.add(childContent);
467        }
468    }
469    
470    private void _applyChanges(WorkflowAwareContent content) throws WorkflowException
471    {
472        Map<String, OutgoingReferences> outgoingReferencesByPath = _outgoingReferencesExtractor.getOutgoingReferences(content);
473        ((ModifiableContent) content).setOutgoingReferences(outgoingReferencesByPath);
474        
475        content.saveChanges();
476        ((DefaultAmetysObject) content).checkpoint();
477        
478        // Notify listeners
479        Map<String, Object> eventParams = new HashMap<>();
480        eventParams.put(ObservationConstants.ARGS_CONTENT, content);
481        eventParams.put(ObservationConstants.ARGS_CONTENT_ID, content.getId());
482        
483        _observationManager.notify(new Event(ObservationConstants.EVENT_CONTENT_MODIFIED, _userProvider.getUser(), eventParams));
484       
485        _contentWorkflowHelper.doAction(content, 22);
486    }
487
488    /**
489     * Synchronize user repeater for orgunit content
490     * @param orgUnitContent the orgunit content
491     * @param orgUnitIdValue the orgUnit id value
492     * @param logger the logger
493     * @return true if changes
494     */
495    protected boolean _synchronizeUserRepeaterOperation(ModifiableDefaultContent orgUnitContent, String orgUnitIdValue, Logger logger)
496    {
497        boolean hasChanges = false;
498        
499        Map<String, String> orgUnitUsers = _orgUnitUsers.get(orgUnitIdValue);
500        if (orgUnitUsers != null)
501        {
502            hasChanges = _synchronizeUserRepeater(orgUnitContent, orgUnitUsers, logger);
503        }
504        
505        return hasChanges;
506    }
507
508    /**
509     * True if the user repeater needs changes
510     * @param orgUnitContent the orgUnit content
511     * @param orgUnitUsers the map of orgunit users
512     * @param logger the logger
513     * @return true if the user repeater needs changes
514     */
515    protected boolean _synchronizeUserRepeater(ModifiableDefaultContent orgUnitContent, Map<String, String> orgUnitUsers, Logger logger)
516    {
517        boolean hasChanges = false;
518        String roleUserColumnName = getRoleUserColumnName();
519        
520        ModifiableCompositeMetadata userRepeater = orgUnitContent.getMetadataHolder().getCompositeMetadata(ORGUNIT_USERS_METADATA, true);
521        
522        int index = 1;
523        List<String> repeaterLogins = new ArrayList<>();
524        String[] metadataNames = userRepeater.getMetadataNames();
525        ArrayUtils.reverse(metadataNames);
526        for (String metadataName : metadataNames)
527        {
528            ModifiableCompositeMetadata entry = userRepeater.getCompositeMetadata(metadataName);
529            
530            String userId = entry.getString(ORGUNIT_USER_METADATA, null);
531            String role = entry.getString(ORGUNIT_USER_ROLE_METADATA, null);
532            
533            try
534            {
535                ModifiableDefaultContent content = _resolver.resolveById(userId);
536                String orgUnitIdValue = _getUserIdValue(content, logger);
537                if (!orgUnitUsers.containsKey(orgUnitIdValue))
538                {
539                    removeEntry(Integer.valueOf(metadataName), userRepeater);
540                    hasChanges = true;
541                    index--;
542                }
543                else if (StringUtils.isNotBlank(roleUserColumnName) && !StringUtils.equals(role, orgUnitUsers.get(orgUnitIdValue)))
544                {
545                    String newRole = orgUnitUsers.get(orgUnitIdValue);
546                    if (StringUtils.isNotBlank(newRole))
547                    {
548                        entry.setMetadata(ORGUNIT_USER_ROLE_METADATA, newRole);
549                    }
550                    else
551                    {
552                        entry.removeMetadata(ORGUNIT_USER_ROLE_METADATA);
553                    }
554                    hasChanges = true;
555                }
556                repeaterLogins.add(orgUnitIdValue);
557            }
558            catch (UnknownAmetysObjectException e) 
559            {
560                logger.warn("Can't find the content user in the repeater with id '" + userId + "'. So it will be deleted", e);
561                removeEntry(Integer.valueOf(metadataName), userRepeater);
562                hasChanges = true;
563                index--;
564            }
565            
566            index++;
567        }
568        
569        String lang = orgUnitContent.getLanguage();
570        for (String login : orgUnitUsers.keySet())
571        {
572            if (!repeaterLogins.contains(login))
573            {
574                Content userContent = _getUserContent(login, lang, logger);
575                if (userContent != null)
576                {
577                    ModifiableCompositeMetadata newEntry = userRepeater.getCompositeMetadata(String.valueOf(index), true);
578                    newEntry.setMetadata(ORGUNIT_USER_METADATA, userContent.getId());
579                    
580                    String role = orgUnitUsers.get(login);
581                    if (StringUtils.isNotBlank(role))
582                    {
583                        newEntry.setMetadata(ORGUNIT_USER_ROLE_METADATA, role);
584                    }
585                    
586                    hasChanges = true;
587                    index++;
588                }
589                else
590                {
591                    logger.warn("Can't add user '" + login + "' to orgunit '" + orgUnitContent.getTitle() + "' because he doesn't exist"); 
592                }
593            }
594        }
595        
596        return hasChanges;
597    }
598    
599    /**
600     * Removes the repeater entry at the given position. The position starts at index 1.
601     * The position can be an integer between 1 and the repeater size to remove an entry from the beginning
602     * 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)
603     * @param position The position of the entry to remove
604     * @param repositoryComposite the repository composite of the repeater
605     * @throws IllegalArgumentException if the position is not between the negative and positive repeater size
606     */
607    protected void removeEntry(int position, ModifiableCompositeMetadata repositoryComposite)
608    {
609        int size = repositoryComposite.getMetadataNames().length;
610        if (1 <= position && position <= size)
611        {
612            // remove the entry
613            repositoryComposite.removeMetadata(String.valueOf(position));
614        
615            // rename all entries after the removed one
616            for (int currentEntryPosition = position + 1; currentEntryPosition <= size; currentEntryPosition++)
617            {
618                ModifiableCompositeMetadata entryRepositoryComposite = repositoryComposite.getCompositeMetadata(String.valueOf(currentEntryPosition));
619                entryRepositoryComposite.rename(RepositoryConstants.NAMESPACE_PREFIX + ":" + String.valueOf(currentEntryPosition - 1));
620            }
621        }
622        else if (-size < position && position <= 0)
623        {
624            // Find the positive equivalent position and call the removeEntry method with this position
625            removeEntry(size + position, repositoryComposite);
626        }
627        else
628        {
629            throw new IllegalArgumentException("The repeater has '" + size + "' entries. You can not remove an entry at position '" + position + "'.");
630        }
631    }
632    
633    /**
634     * Get user content from login value
635     * @param loginValue the login value
636     * @param lang the language
637     * @param logger the logger
638     * @return the user content
639     */
640    protected Content _getUserContent(String loginValue, String lang, Logger logger)
641    {
642        String loginMetadata = getLoginUserMetadataName();
643        Set<String> contentTypes = _contentTypeEP.getSubTypes(UserDirectoryPageHandler.ABSTRACT_USER_CONTENT_TYPE);
644        
645        Expression ctypeExpression = new ContentTypeExpression(Operator.EQ, contentTypes.toArray(new String[contentTypes.size()]));
646        LanguageExpression languageExpression = new LanguageExpression(Operator.EQ, lang);
647        StringExpression metadataExp = new StringExpression(loginMetadata, Operator.EQ, loginValue);
648        
649        Expression userExp = new AndExpression(metadataExp, ctypeExpression, languageExpression);
650        String xPathQuery = ContentQueryHelper.getContentXPathQuery(userExp);
651        
652        AmetysObjectIterable<Content> contentQuery = _resolver.query(xPathQuery);
653        AmetysObjectIterator<Content> contentIterator = contentQuery.iterator();
654        if (contentIterator.hasNext())
655        {
656            return contentIterator.next();
657        }
658        
659        return null;
660    }
661    
662    /**
663     * Get orgunit content from the remote Id
664     * @param remoteId the remote Id
665     * @param lang the language
666     * @param logger the logger
667     * @return the orgunit content
668     */
669    protected Content _getOrgUnitContentFromRemoteId(String remoteId, String lang, Logger logger)
670    {
671        Expression ctypeExpression = new ContentTypeExpression(Operator.EQ, OrganisationChartPageHandler.ORGUNIT_CONTENT_TYPE);
672        RemoteIdOrgunitExpression remoteIdOrgunitExpression = new RemoteIdOrgunitExpression(remoteId);
673        LanguageExpression languageExpression = new LanguageExpression(Operator.EQ, lang);
674        
675        Expression userExp = new AndExpression(remoteIdOrgunitExpression, ctypeExpression, languageExpression);
676        String xPathQuery = ContentQueryHelper.getContentXPathQuery(userExp);
677        
678        AmetysObjectIterable<Content> contentQuery = _resolver.query(xPathQuery);
679        AmetysObjectIterator<Content> contentIterator = contentQuery.iterator();
680        if (contentIterator.hasNext())
681        {
682            return contentIterator.next();
683        }
684        
685        return null;
686    }
687    
688    /**
689     * Get the parameters map for user mybatis search
690     * @param orgUnitColumnKey the column name of the orgunit key
691     * @param logger the logger
692     * @return the parameter map
693     */
694    protected Map<String, Object> _getSearchUserParameters(String orgUnitColumnKey, Logger logger)
695    {
696        Map<String, Object> params = new HashMap<>();
697        params.put("loginColumnName", getLoginUserColumnName());
698        params.put("tableUser", getUserTableName());
699        params.put("tableOrgUnit", getTableName());
700        params.put("joinColumnName", getOrgunitJoinColumnNameForUser());
701        params.put("orgUnitColumnKey", orgUnitColumnKey);
702        params.put("orgUnitIdColumnName", getOrgUnitRemoteIdColumnName());
703        
704        String roleUserColumnName = getRoleUserColumnName();
705        if (StringUtils.isNotBlank(roleUserColumnName))
706        {
707            params.put("roleColumnName", roleUserColumnName);
708        }
709
710        return params;
711    }
712    
713    /**
714     * Get the user id value
715     * @param userContent the user content
716     * @param logger the logger
717     * @return the user id value
718     */
719    protected String _getUserIdValue(ModifiableDefaultContent userContent, Logger logger)
720    {
721        String loginMetadataName = getLoginUserMetadataName();
722        return userContent.getMetadataHolder().getString(loginMetadataName, null);
723    }
724    
725    @Override
726    @SuppressWarnings("unchecked")
727    protected void deleteUnexistingContents(Logger logger)
728    {
729        String query = _getContentPathQuery(null, null, null);
730        AmetysObjectIterable<ModifiableDefaultContent> contents = _resolver.query(query);
731        
732        List<Content> contentsToRemove = _getContentsToRemove(contents);
733        
734        contentsToRemove.stream().forEach(content -> logger.info("The content '{}' ({}) does not exist anymore in remote source: it will be deleted if possible.", content.getTitle(), content.getId()));
735        
736        List<String> contentIds = contentsToRemove.stream()
737                .map(Content::getId)
738                .collect(Collectors.toList());
739        
740        logger.info("Trying to delete contents. This can take a while...");
741        Map<String, Object> deleteResults = _deleteOrgUnitComponent.deleteContents(contentIds, MapUtils.EMPTY_SORTED_MAP, MapUtils.EMPTY_SORTED_MAP);
742        logger.info("Contents deleting process ended.");
743        
744        for (String contentId : contentIds)
745        {
746            Map<String, Object> result = (Map<String, Object>) deleteResults.get(contentId);
747            List<String> deletedContents = (List<String>) result.get("deleted-contents");
748            _nbDeletedContents += deletedContents.size();
749            
750            List<Content> referencedContents = (List<Content>) result.get("referenced-contents");
751            if (referencedContents.size() > 0)
752            {
753                logger.info("The following contents cannot be deleted because they are referenced: {}", referencedContents.stream().map(c -> c.getId()).collect(Collectors.toList()));
754            }
755            
756            List<Content> lockedContents = (List<Content>) result.get("locked-contents");
757            if (lockedContents.size() > 0)
758            {
759                logger.info("The following contents cannot be deleted because they are locked: {}", lockedContents.stream().map(c -> c.getId()).collect(Collectors.toList()));
760            }
761            
762            List<Content> undeletedContents = (List<Content>) result.get("undeleted-contents");
763            if (undeletedContents.size() > 0)
764            {
765                logger.info("{} contents were not deleted. See previous logs for more information.", undeletedContents.size());
766            }
767        }
768        
769    }
770}