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}