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