From dd0bfaba82ac42a3cb01976ae360f2f8f43da71c Mon Sep 17 00:00:00 2001
From: srosse <none@none>
Date: Wed, 28 Nov 2012 14:45:43 +0100
Subject: [PATCH] OO-428: implements deduplicate tool for course and groups

---
 .../progressbar/ProgressController.java       |  75 ++++++++++++
 .../progressbar/_content/progress.html        |   4 +
 .../generic/iframe/IFrameDeliveryMapper.java  |  19 +++
 .../core/util/async/ProgressDelegate.java     |  34 ++++++
 .../member/MembersOverviewController.java     |  47 +++++++-
 .../member/_content/members_overview.html     |   3 +
 .../member/_i18n/LocalStrings_de.properties   |   2 +
 .../org/olat/group/BusinessGroupService.java  |   9 ++
 .../manager/BusinessGroupServiceImpl.java     |  92 ++++++++++++++
 .../BusinessGroupModuleAdminController.java   | 112 ++++++++++++++++--
 .../org/olat/group/ui/_content/bg_admin.html  |  13 ++
 .../group/ui/_i18n/LocalStrings_de.properties |   4 +
 .../DedupMembersConfirmationController.java   |  91 ++++++++++++++
 .../olat/group/ui/main/_content/dedup.html    |   3 +
 .../ui/main/_i18n/LocalStrings_de.properties  |   5 +
 .../olat/repository/RepositoryManager.java    |   2 -
 .../java/org/olat/user/restapi/RolesVO.java   |  19 +++
 .../document/file/OfficeDocumentTest.java     |  19 +++
 18 files changed, 541 insertions(+), 12 deletions(-)
 create mode 100644 src/main/java/org/olat/core/gui/components/progressbar/ProgressController.java
 create mode 100644 src/main/java/org/olat/core/gui/components/progressbar/_content/progress.html
 create mode 100644 src/main/java/org/olat/core/util/async/ProgressDelegate.java
 create mode 100644 src/main/java/org/olat/group/ui/_content/bg_admin.html
 create mode 100644 src/main/java/org/olat/group/ui/main/DedupMembersConfirmationController.java
 create mode 100644 src/main/java/org/olat/group/ui/main/_content/dedup.html

diff --git a/src/main/java/org/olat/core/gui/components/progressbar/ProgressController.java b/src/main/java/org/olat/core/gui/components/progressbar/ProgressController.java
new file mode 100644
index 00000000000..77a9c0d593f
--- /dev/null
+++ b/src/main/java/org/olat/core/gui/components/progressbar/ProgressController.java
@@ -0,0 +1,75 @@
+/**
+ * <a href="http://www.openolat.org">
+ * OpenOLAT - Online Learning and Training</a><br>
+ * <p>
+ * Licensed under the Apache License, Version 2.0 (the "License"); <br>
+ * you may not use this file except in compliance with the License.<br>
+ * You may obtain a copy of the License at the
+ * <a href="http://www.apache.org/licenses/LICENSE-2.0">Apache homepage</a>
+ * <p>
+ * Unless required by applicable law or agreed to in writing,<br>
+ * software distributed under the License is distributed on an "AS IS" BASIS, <br>
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. <br>
+ * See the License for the specific language governing permissions and <br>
+ * limitations under the License.
+ * <p>
+ * Initial code contributed and copyrighted by<br>
+ * frentix GmbH, http://www.frentix.com
+ * <p>
+ */
+package org.olat.core.gui.components.progressbar;
+
+import org.olat.core.gui.UserRequest;
+import org.olat.core.gui.components.Component;
+import org.olat.core.gui.components.velocity.VelocityContainer;
+import org.olat.core.gui.control.Event;
+import org.olat.core.gui.control.WindowControl;
+import org.olat.core.gui.control.controller.BasicController;
+import org.olat.core.util.async.ProgressDelegate;
+
+/**
+ * A controller which use a procentual progress bar filled at 100% at start.
+ * 
+ * Initial date: 28.11.2012<br>
+ * @author srosse, stephane.rosse@frentix.com, http://www.frentix.com
+ *
+ */
+public class ProgressController extends BasicController implements ProgressDelegate {
+
+	private final VelocityContainer mainVC;
+	private final ProgressBar progressBar;
+	
+	public ProgressController(UserRequest ureq, WindowControl wControl) {
+		super(ureq, wControl);
+		
+		mainVC = createVelocityContainer("progress");
+		progressBar = new ProgressBar("dedup", 600, 0.0f, 100.0f, "%");
+		progressBar.setActual(100.0f);
+		mainVC.put("progress", progressBar);
+		putInitialPanel(mainVC);
+	}
+	
+	@Override
+	protected void doDispose() {
+		//
+	}
+	
+	public void setMessage(String translatedMsg) {
+		mainVC.contextPut("msg", translatedMsg);
+	}
+
+	@Override
+	public void setActual(float i) {
+		progressBar.setActual(i);
+	}
+
+	@Override
+	public void finished() {
+		progressBar.setActual(0.0f);
+	}
+
+	@Override
+	protected void event(UserRequest ureq, Component source, Event event) {
+		//
+	}
+}
diff --git a/src/main/java/org/olat/core/gui/components/progressbar/_content/progress.html b/src/main/java/org/olat/core/gui/components/progressbar/_content/progress.html
new file mode 100644
index 00000000000..8cba9133801
--- /dev/null
+++ b/src/main/java/org/olat/core/gui/components/progressbar/_content/progress.html
@@ -0,0 +1,4 @@
+#if($msg)
+<p>$msg</p>
+#end
+$r.render("progress")
\ No newline at end of file
diff --git a/src/main/java/org/olat/core/gui/control/generic/iframe/IFrameDeliveryMapper.java b/src/main/java/org/olat/core/gui/control/generic/iframe/IFrameDeliveryMapper.java
index 1abd65cb897..90637100c71 100644
--- a/src/main/java/org/olat/core/gui/control/generic/iframe/IFrameDeliveryMapper.java
+++ b/src/main/java/org/olat/core/gui/control/generic/iframe/IFrameDeliveryMapper.java
@@ -1,3 +1,22 @@
+/**
+ * <a href="http://www.openolat.org">
+ * OpenOLAT - Online Learning and Training</a><br>
+ * <p>
+ * Licensed under the Apache License, Version 2.0 (the "License"); <br>
+ * you may not use this file except in compliance with the License.<br>
+ * You may obtain a copy of the License at the
+ * <a href="http://www.apache.org/licenses/LICENSE-2.0">Apache homepage</a>
+ * <p>
+ * Unless required by applicable law or agreed to in writing,<br>
+ * software distributed under the License is distributed on an "AS IS" BASIS, <br>
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. <br>
+ * See the License for the specific language governing permissions and <br>
+ * limitations under the License.
+ * <p>
+ * Initial code contributed and copyrighted by<br>
+ * frentix GmbH, http://www.frentix.com
+ * <p>
+ */
 package org.olat.core.gui.control.generic.iframe;
 
 import java.io.Serializable;
diff --git a/src/main/java/org/olat/core/util/async/ProgressDelegate.java b/src/main/java/org/olat/core/util/async/ProgressDelegate.java
new file mode 100644
index 00000000000..2e060fef023
--- /dev/null
+++ b/src/main/java/org/olat/core/util/async/ProgressDelegate.java
@@ -0,0 +1,34 @@
+/**
+ * <a href="http://www.openolat.org">
+ * OpenOLAT - Online Learning and Training</a><br>
+ * <p>
+ * Licensed under the Apache License, Version 2.0 (the "License"); <br>
+ * you may not use this file except in compliance with the License.<br>
+ * You may obtain a copy of the License at the
+ * <a href="http://www.apache.org/licenses/LICENSE-2.0">Apache homepage</a>
+ * <p>
+ * Unless required by applicable law or agreed to in writing,<br>
+ * software distributed under the License is distributed on an "AS IS" BASIS, <br>
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. <br>
+ * See the License for the specific language governing permissions and <br>
+ * limitations under the License.
+ * <p>
+ * Initial code contributed and copyrighted by<br>
+ * frentix GmbH, http://www.frentix.com
+ * <p>
+ */
+package org.olat.core.util.async;
+
+/**
+ * 
+ * Initial date: 28.11.2012<br>
+ * @author srosse, stephane.rosse@frentix.com, http://www.frentix.com
+ *
+ */
+public interface ProgressDelegate {
+	
+	public void setActual(float value);
+	
+	public void finished();
+
+}
diff --git a/src/main/java/org/olat/course/member/MembersOverviewController.java b/src/main/java/org/olat/course/member/MembersOverviewController.java
index f9de2c94df4..241aec8081f 100644
--- a/src/main/java/org/olat/course/member/MembersOverviewController.java
+++ b/src/main/java/org/olat/course/member/MembersOverviewController.java
@@ -35,6 +35,7 @@ import org.olat.core.gui.control.Controller;
 import org.olat.core.gui.control.Event;
 import org.olat.core.gui.control.WindowControl;
 import org.olat.core.gui.control.controller.BasicController;
+import org.olat.core.gui.control.generic.closablewrapper.CloseableModalController;
 import org.olat.core.gui.control.generic.dtabs.Activateable2;
 import org.olat.core.gui.control.generic.wizard.Step;
 import org.olat.core.gui.control.generic.wizard.StepRunnerCallback;
@@ -57,6 +58,7 @@ import org.olat.course.member.wizard.ImportMember_1a_LoginListStep;
 import org.olat.course.member.wizard.ImportMember_1b_ChooseMemberStep;
 import org.olat.group.BusinessGroupService;
 import org.olat.group.model.BusinessGroupMembershipChange;
+import org.olat.group.ui.main.DedupMembersConfirmationController;
 import org.olat.repository.RepositoryEntry;
 import org.olat.repository.RepositoryManager;
 import org.olat.repository.model.RepositoryEntryPermissionChangeEvent;
@@ -88,9 +90,11 @@ public class MembersOverviewController extends BasicController implements Activa
 	private AbstractMemberListController waitingCtrl;
 	private AbstractMemberListController selectedCtrl;
 	private AbstractMemberListController searchCtrl;
-	private final Link importMemberLink, addMemberLink;
-	
+	private final Link importMemberLink, addMemberLink, dedupLink;
+
+	private CloseableModalController cmc;
 	private StepsMainRunController importMembersWizard;
+	private DedupMembersConfirmationController dedupCtrl;
 	
 	private final RepositoryEntry repoEntry;
 	private final RepositoryManager repositoryManager;
@@ -118,12 +122,14 @@ public class MembersOverviewController extends BasicController implements Activa
 		searchLink = LinkFactory.createLink("search", mainVC, this);
 		segmentView.addSegment(searchLink, false);
 		
-		updateAllMembers(ureq);
+		selectedCtrl = updateAllMembers(ureq);
 		
 		addMemberLink = LinkFactory.createButton("add.member", mainVC, this);
 		mainVC.put("addMembers", addMemberLink);
 		importMemberLink = LinkFactory.createButton("import.member", mainVC, this);
 		mainVC.put("importMembers", importMemberLink);
+		dedupLink = LinkFactory.createButton("dedup.members", mainVC, this);
+		mainVC.put("dedupMembers", dedupLink);
 		
 		putInitialPanel(mainVC);
 	}
@@ -185,6 +191,8 @@ public class MembersOverviewController extends BasicController implements Activa
 			doChooseMembers(ureq);
 		} else if (source == importMemberLink) {
 			doImportMembers(ureq);
+		} else if (source == dedupLink) {
+			doDedupMembers(ureq);
 		}
 	}
 
@@ -201,9 +209,24 @@ public class MembersOverviewController extends BasicController implements Activa
 					}
 				}
 			}
+		} else if(source == dedupCtrl) {
+			cmc.deactivate();
+			if(event == Event.DONE_EVENT) {
+				dedupMembers(ureq, dedupCtrl.isDedupCoaches(), dedupCtrl.isDedupParticipants());
+			}
+			cleanUp();
+		} else if(source == cmc) {
+			cleanUp();
 		}
 		super.event(ureq, source, event);
 	}
+	
+	private void cleanUp() {
+		removeAsListenerAndDispose(dedupCtrl);
+		removeAsListenerAndDispose(cmc);
+		dedupCtrl = null;
+		cmc = null;
+	}
 
 	private void doChooseMembers(UserRequest ureq) {
 		removeAsListenerAndDispose(importMembersWizard);
@@ -265,6 +288,24 @@ public class MembersOverviewController extends BasicController implements Activa
 		switchToAllMembers(ureq);
 	}
 	
+	protected void doDedupMembers(UserRequest ureq) {
+		dedupCtrl = new DedupMembersConfirmationController(ureq, getWindowControl());
+		listenTo(dedupCtrl);
+		
+		cmc = new CloseableModalController(getWindowControl(), translate("close"), dedupCtrl.getInitialComponent(),
+				true, translate("dedup.members"));
+		cmc.activate();
+		listenTo(cmc);
+	}
+	
+	protected void dedupMembers(UserRequest ureq, boolean coaches, boolean participants) {
+		businessGroupService.dedupMembers(ureq.getIdentity(), repoEntry, coaches, participants);
+		showInfo("dedup.done");
+		if(selectedCtrl != null) {
+			selectedCtrl.reloadModel();
+		}
+	}
+	
 	private void switchToAllMembers(UserRequest ureq) {
 		DBFactory.getInstance().commit();//make sure all is on the DB before reloading
 		if(selectedCtrl != null && selectedCtrl == allMemberListCtrl) {
diff --git a/src/main/java/org/olat/course/member/_content/members_overview.html b/src/main/java/org/olat/course/member/_content/members_overview.html
index 77bd534159b..c0a3a83a73e 100644
--- a/src/main/java/org/olat/course/member/_content/members_overview.html
+++ b/src/main/java/org/olat/course/member/_content/members_overview.html
@@ -5,6 +5,9 @@
 	#if($r.available("importMembers"))
 		$r.render("importMembers")
 	#end
+	#if($r.available("dedupMembers"))
+		$r.render("dedupMembers")
+	#end
 </div>
 <h4 class="b_with_small_icon_left b_group_icon">
 	$r.translate("menu.members")
diff --git a/src/main/java/org/olat/course/member/_i18n/LocalStrings_de.properties b/src/main/java/org/olat/course/member/_i18n/LocalStrings_de.properties
index 68887a36c01..f1ef8e18224 100644
--- a/src/main/java/org/olat/course/member/_i18n/LocalStrings_de.properties
+++ b/src/main/java/org/olat/course/member/_i18n/LocalStrings_de.properties
@@ -5,6 +5,8 @@ assessment=Bewertungswerkzeug
 course.lastTime=Zuletzt ge\u00F6fnnet
 course.membership.creation=Kurs Beitritt
 course.numOfVisits=Anzahl Kursaufrufe
+dedup.members=Mitglieder deduplizieren
+dedup.done=Deduplizierung wurde erfolgreich beendet
 dialog.modal.bg.leave.text=Wollen Sie wirklich die Person(en) {0} aus dem Kurs und allen Gruppen entfernen?
 edit.member=Mitglied bearbeiten
 edit.member.groups=Gruppenmitgliedschaften
diff --git a/src/main/java/org/olat/group/BusinessGroupService.java b/src/main/java/org/olat/group/BusinessGroupService.java
index 55829e071db..9f686b127a0 100644
--- a/src/main/java/org/olat/group/BusinessGroupService.java
+++ b/src/main/java/org/olat/group/BusinessGroupService.java
@@ -26,6 +26,7 @@ import java.util.Locale;
 
 import org.olat.basesecurity.SecurityGroup;
 import org.olat.core.id.Identity;
+import org.olat.core.util.async.ProgressDelegate;
 import org.olat.core.util.mail.MailerResult;
 import org.olat.group.area.BGArea;
 import org.olat.group.model.BGRepositoryEntryRelation;
@@ -443,6 +444,14 @@ public interface BusinessGroupService {
 	public BusinessGroupAddResponse addToSecurityGroupAndFireEvent(Identity ureqIdentity, List<Identity> addIdentities, SecurityGroup secGroup);
 	
 	public void removeAndFireEvent(Identity ureqIdentity, List<Identity> addIdentities, SecurityGroup secGroup);
+	
+	/**
+	 * Remove the members of the repository entry which are already in a business group
+	 * linked to it.
+	 */
+	public void dedupMembers(Identity ureqIdentity, RepositoryEntry entry, boolean coaches, boolean participants);
+	
+	public void dedupMembers(Identity ureqIdentity, boolean coaches, boolean participants, ProgressDelegate progressDelegate);
 
 	
 	//security
diff --git a/src/main/java/org/olat/group/manager/BusinessGroupServiceImpl.java b/src/main/java/org/olat/group/manager/BusinessGroupServiceImpl.java
index af78d25836e..d89660ba55c 100644
--- a/src/main/java/org/olat/group/manager/BusinessGroupServiceImpl.java
+++ b/src/main/java/org/olat/group/manager/BusinessGroupServiceImpl.java
@@ -46,12 +46,14 @@ import org.olat.collaboration.CollaborationToolsFactory;
 import org.olat.core.commons.persistence.DB;
 import org.olat.core.commons.taskExecutor.TaskExecutorManager;
 import org.olat.core.id.Identity;
+import org.olat.core.id.Roles;
 import org.olat.core.logging.DBRuntimeException;
 import org.olat.core.logging.KnownIssueException;
 import org.olat.core.logging.OLog;
 import org.olat.core.logging.Tracing;
 import org.olat.core.logging.activity.ActionType;
 import org.olat.core.logging.activity.ThreadLocalUserActivityLogger;
+import org.olat.core.util.async.ProgressDelegate;
 import org.olat.core.util.coordinate.CoordinatorManager;
 import org.olat.core.util.mail.MailContext;
 import org.olat.core.util.mail.MailContextImpl;
@@ -98,6 +100,8 @@ import org.olat.instantMessaging.syncservice.SyncUserListTask;
 import org.olat.properties.Property;
 import org.olat.repository.RepositoryEntry;
 import org.olat.repository.RepositoryEntryShort;
+import org.olat.repository.RepositoryManager;
+import org.olat.repository.SearchRepositoryEntryParameters;
 import org.olat.resource.OLATResource;
 import org.olat.resource.accesscontrol.ACService;
 import org.olat.resource.accesscontrol.model.ResourceReservation;
@@ -124,6 +128,8 @@ public class BusinessGroupServiceImpl implements BusinessGroupService, UserDataD
 	@Autowired
 	private BusinessGroupDAO businessGroupDAO;
 	@Autowired
+	private RepositoryManager repositoryManager;
+	@Autowired
 	private BaseSecurity securityManager;
 	@Autowired
 	private BusinessGroupRelationDAO businessGroupRelationDAO;
@@ -1371,6 +1377,92 @@ public class BusinessGroupServiceImpl implements BusinessGroupService, UserDataD
 		}
 	}
 
+	@Override
+	public void dedupMembers(Identity ureqIdentity, boolean coaches, boolean participants, ProgressDelegate delegate) {
+		SearchRepositoryEntryParameters params = new SearchRepositoryEntryParameters();
+		params.setRoles(new Roles(true, false, false, false, false, false, false));
+		params.setResourceTypes(Collections.singletonList("CourseModule"));
+		
+		float ratio = -1.0f;
+		if(delegate != null) {
+			int numOfEntries = repositoryManager.countGenericANDQueryWithRolesRestriction(params, true);
+			ratio = 100.0f / (float)numOfEntries;
+		}
+
+		int counter = 0;
+		int countForCommit = 0;
+		float actual = 100.0f;
+		int batch = 25;
+		List<RepositoryEntry> entries;
+		do {
+			entries = repositoryManager.genericANDQueryWithRolesRestriction(params, counter, batch, true);
+			for(RepositoryEntry re:entries) {
+				countForCommit += dedupSingleRepositoryentry(ureqIdentity, re, coaches, participants);
+				if(countForCommit > 25) {
+					dbInstance.intermediateCommit();
+					countForCommit = 0;
+				}
+			}
+			counter += entries.size();
+			if(delegate != null) {
+				actual -= (entries.size() * ratio);
+				delegate.setActual(actual);
+			}
+		} while(entries.size() == batch);
+		
+		if(delegate != null) {
+			delegate.finished();
+		}
+	}
+
+	@Override
+	@Transactional
+	public void dedupMembers(Identity ureqIdentity, RepositoryEntry entry, boolean coaches, boolean participants) {
+		dedupSingleRepositoryentry(ureqIdentity, entry, coaches, participants);
+	}
+	
+	private int dedupSingleRepositoryentry(Identity ureqIdentity, RepositoryEntry entry, boolean coaches, boolean participants) {
+		int count = 0;
+		
+		List<BusinessGroup> groups = null;//load only if needed
+		if(coaches) {
+			List<Identity> repoTutorList = securityManager.getIdentitiesOfSecurityGroup(entry.getTutorGroup());
+			if(!repoTutorList.isEmpty()) {
+				SearchBusinessGroupParams params = new SearchBusinessGroupParams();
+				groups = businessGroupDAO.findBusinessGroups(params, entry.getOlatResource(), 0, -1);
+				List<SecurityGroup> ownerSecGroups = new ArrayList<SecurityGroup>();
+				for(BusinessGroup group:groups) {
+					ownerSecGroups.add(group.getOwnerGroup());
+				}
+				
+				List<Identity> ownerList = securityManager.getIdentitiesOfSecurityGroups(ownerSecGroups);
+				repoTutorList.retainAll(ownerList);
+				repositoryManager.removeTutors(null, repoTutorList, entry);
+				count += repoTutorList.size();
+			}
+		}
+		
+		if(participants) {
+			List<Identity> repoParticipantList = securityManager.getIdentitiesOfSecurityGroup(entry.getParticipantGroup());
+			if(!repoParticipantList.isEmpty()) {
+			
+				if(groups == null) {
+					SearchBusinessGroupParams params = new SearchBusinessGroupParams();
+					groups = businessGroupDAO.findBusinessGroups(params, entry.getOlatResource(), 0, -1);
+				}
+				List<SecurityGroup> participantSecGroups = new ArrayList<SecurityGroup>();
+				for(BusinessGroup group:groups) {
+					participantSecGroups.add(group.getPartipiciantGroup());
+				}
+				List<Identity> participantList = securityManager.getIdentitiesOfSecurityGroups(participantSecGroups);
+				repoParticipantList.retainAll(participantList);
+				repositoryManager.removeParticipants(ureqIdentity, repoParticipantList, entry);
+				count += participantList.size();
+			}
+		}
+		return 2 + count;
+	}
+
 	@Override
 	@Transactional
 	public void removeResourceFrom(BusinessGroup group, RepositoryEntry re) {
diff --git a/src/main/java/org/olat/group/ui/BusinessGroupModuleAdminController.java b/src/main/java/org/olat/group/ui/BusinessGroupModuleAdminController.java
index b570fcecd83..e99d4169f4a 100644
--- a/src/main/java/org/olat/group/ui/BusinessGroupModuleAdminController.java
+++ b/src/main/java/org/olat/group/ui/BusinessGroupModuleAdminController.java
@@ -20,42 +20,61 @@
 package org.olat.group.ui;
 
 import org.olat.core.CoreSpringFactory;
+import org.olat.core.commons.taskExecutor.TaskExecutorManager;
 import org.olat.core.gui.UserRequest;
+import org.olat.core.gui.components.form.flexible.FormItem;
 import org.olat.core.gui.components.form.flexible.FormItemContainer;
+import org.olat.core.gui.components.form.flexible.elements.FormLink;
 import org.olat.core.gui.components.form.flexible.elements.MultipleSelectionElement;
 import org.olat.core.gui.components.form.flexible.impl.FormBasicController;
+import org.olat.core.gui.components.form.flexible.impl.FormEvent;
 import org.olat.core.gui.components.form.flexible.impl.FormLayoutContainer;
+import org.olat.core.gui.components.link.Link;
+import org.olat.core.gui.components.panel.Panel;
+import org.olat.core.gui.components.progressbar.ProgressController;
 import org.olat.core.gui.control.Controller;
+import org.olat.core.gui.control.Event;
 import org.olat.core.gui.control.WindowControl;
+import org.olat.core.gui.control.generic.closablewrapper.CloseableModalController;
+import org.olat.core.util.async.ProgressDelegate;
 import org.olat.group.BusinessGroupModule;
+import org.olat.group.BusinessGroupService;
+import org.olat.group.ui.main.DedupMembersConfirmationController;
 
 /**
  * 
  * @author srosse, stephane.rosse@frentix.com, http://www.frentix.com
  */
-public class BusinessGroupModuleAdminController extends FormBasicController {
+public class BusinessGroupModuleAdminController extends FormBasicController implements ProgressDelegate {
 	
+	private FormLink dedupLink;
 	private MultipleSelectionElement allowEl;
+
+	private Panel mainPopPanel;
+	private CloseableModalController cmc;
+	private ProgressController progressCtrl;
+	private DedupMembersConfirmationController dedupCtrl;
 	
 	private final BusinessGroupModule module;
+	private final BusinessGroupService businessGroupService;
 	private String[] onKeys = new String[]{"user","author"};
 	
 	public BusinessGroupModuleAdminController(UserRequest ureq, WindowControl wControl) {
-		super(ureq, wControl);
+		super(ureq, wControl, "bg_admin");
 		module = CoreSpringFactory.getImpl(BusinessGroupModule.class);
+		businessGroupService = CoreSpringFactory.getImpl(BusinessGroupService.class);
 		initForm(ureq);
 	}
 
 	@Override
 	protected void initForm(FormItemContainer formLayout, Controller listener, UserRequest ureq) {
-		setFormTitle("module.admin.title");
-		setFormDescription("module.admin.desc");
-		
+		FormLayoutContainer optionsContainer = FormLayoutContainer.createDefaultFormLayout("options", getTranslator());
+		formLayout.add(optionsContainer);
 		String[] values = new String[]{
 				translate("user.allow.create"),
 				translate("author.allow.create")
 		};
-		allowEl = uifactory.addCheckboxesVertical("module.admin.allow.create", formLayout, onKeys, values, null, 1);
+		allowEl = uifactory.addCheckboxesVertical("module.admin.allow.create", optionsContainer, onKeys, values, null, 1);
 		allowEl.select("user", module.isUserAllowedCreate());
 		allowEl.select("author", module.isAuthorAllowedCreate());
 		
@@ -63,16 +82,95 @@ public class BusinessGroupModuleAdminController extends FormBasicController {
 		buttonsContainer.setRootForm(mainForm);
 		formLayout.add(buttonsContainer);
 		uifactory.addFormSubmitButton("ok", "ok", formLayout);
+		
+		FormLayoutContainer dedupCont = FormLayoutContainer.createDefaultFormLayout("dedup", getTranslator());
+		formLayout.add(dedupCont);
+		dedupLink = uifactory.addFormLink("dedup.members", dedupCont, Link.BUTTON);
 	}
 	
 	@Override
 	protected void doDispose() {
 		//
 	}
+	
+	@Override
+	protected void event(UserRequest ureq, Controller source, Event event) {
+		if(source == dedupCtrl) {
+			boolean coaches = dedupCtrl.isDedupCoaches();
+			boolean participants = dedupCtrl.isDedupParticipants();
+			if(event == Event.DONE_EVENT) {
+				dedupMembers(ureq, coaches, participants);
+			} else {
+				cmc.deactivate();
+				cleanUp();
+			}
+		} else if(source == cmc) {
+			cleanUp();
+		}
+		super.event(ureq, source, event);
+	}
+	
+	private void cleanUp() {
+		removeAsListenerAndDispose(dedupCtrl);
+		removeAsListenerAndDispose(progressCtrl);
+		removeAsListenerAndDispose(cmc);
+		progressCtrl = null;
+		dedupCtrl = null;
+		cmc = null;
+	}
+
+	@Override
+	public void setActual(float value) {
+		if(progressCtrl != null) {
+			progressCtrl.setActual(value);
+		}
+	}
+
+	@Override
+	public void finished() {
+		cmc.deactivate();
+		cleanUp();
+	}
+
+	@Override
+	protected void formInnerEvent(UserRequest ureq, FormItem source, FormEvent event) {
+		if(source == dedupLink) {
+			doDedupMembers(ureq);
+		} else {
+			super.formInnerEvent(ureq, source, event);
+		}
+	}
+	
+	protected void doDedupMembers(UserRequest ureq) {
+		dedupCtrl = new DedupMembersConfirmationController(ureq, getWindowControl());
+		listenTo(dedupCtrl);
+		
+		mainPopPanel = new Panel("dedup");
+		mainPopPanel.setContent(dedupCtrl.getInitialComponent());
+		
+		cmc = new CloseableModalController(getWindowControl(), translate("close"), mainPopPanel, true, translate("dedup.members"), true);
+		cmc.activate();
+		listenTo(cmc);
+	}
+	
+	protected void dedupMembers(UserRequest ureq, final boolean coaches, final boolean participants) {
+		progressCtrl = new ProgressController(ureq, getWindowControl());
+		progressCtrl.setMessage(translate("dedup.running"));
+		mainPopPanel.setContent(progressCtrl.getInitialComponent());
+		listenTo(progressCtrl);
+		
+		Runnable worker = new Runnable() {
+			@Override
+			public void run() {
+				businessGroupService.dedupMembers(getIdentity(), coaches, participants, BusinessGroupModuleAdminController.this);
+			}
+		};
+		TaskExecutorManager.getInstance().runTask(worker);
+	}
 
 	@Override
 	protected void formOK(UserRequest ureq) {
 		module.setUserAllowedCreate(allowEl.isSelected(0));
 		module.setAuthorAllowedCreate(allowEl.isSelected(1));
 	}
-}
+}
\ No newline at end of file
diff --git a/src/main/java/org/olat/group/ui/_content/bg_admin.html b/src/main/java/org/olat/group/ui/_content/bg_admin.html
new file mode 100644
index 00000000000..724e10e4459
--- /dev/null
+++ b/src/main/java/org/olat/group/ui/_content/bg_admin.html
@@ -0,0 +1,13 @@
+<div class="b_form b_clearfix">
+	<fieldset>
+		<legend>$r.translate("module.admin.title")</legend>
+		<div class="b_form_desc">$r.translate("module.admin.desc")</div>
+		$r.render("options")
+		$r.render("module.buttons")
+	</fieldset>
+	<fieldset>
+		<legend>$r.translate("dedup.members")</legend>
+		<div class="b_form_desc">$r.translate("dedup.members.desc")</div>
+		$r.render("dedup")
+	</fieldset>
+</div>
\ No newline at end of file
diff --git a/src/main/java/org/olat/group/ui/_i18n/LocalStrings_de.properties b/src/main/java/org/olat/group/ui/_i18n/LocalStrings_de.properties
index 08545aa146a..1a64823dc6c 100644
--- a/src/main/java/org/olat/group/ui/_i18n/LocalStrings_de.properties
+++ b/src/main/java/org/olat/group/ui/_i18n/LocalStrings_de.properties
@@ -34,6 +34,10 @@ notification.mail.self.error=Die E-Mail konnte nicht an Sie verschickt werden.
 warn.foldernotavailable=Der Ordner ist momentan nicht aktiviert und kann nicht angezeigt werden.
 warn.forumnotavailable=Das Forum ist momentan nicht aktiviert und kann nicht angezeigt werden.
 
+dedup.members=Mitglieder deduplizieren
+dedup.members.desc=Hier können Sie die Mitglieder entfernen in einer Kurs sowie in einer Grupper dieser Kurs eingetragen sind.
+dedup.running=Deduplizerung ist am Laufen
+dedup.done=Deduplizierung wurde erfolgreich beendet
 
 admin.menu.title=Gruppe
 admin.menu.title.alt=Gruppe
diff --git a/src/main/java/org/olat/group/ui/main/DedupMembersConfirmationController.java b/src/main/java/org/olat/group/ui/main/DedupMembersConfirmationController.java
new file mode 100644
index 00000000000..e6bb69ac60e
--- /dev/null
+++ b/src/main/java/org/olat/group/ui/main/DedupMembersConfirmationController.java
@@ -0,0 +1,91 @@
+/**
+ * <a href="http://www.openolat.org">
+ * OpenOLAT - Online Learning and Training</a><br>
+ * <p>
+ * Licensed under the Apache License, Version 2.0 (the "License"); <br>
+ * you may not use this file except in compliance with the License.<br>
+ * You may obtain a copy of the License at the
+ * <a href="http://www.apache.org/licenses/LICENSE-2.0">Apache homepage</a>
+ * <p>
+ * Unless required by applicable law or agreed to in writing,<br>
+ * software distributed under the License is distributed on an "AS IS" BASIS, <br>
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. <br>
+ * See the License for the specific language governing permissions and <br>
+ * limitations under the License.
+ * <p>
+ * Initial code contributed and copyrighted by<br>
+ * frentix GmbH, http://www.frentix.com
+ * <p>
+ */
+package org.olat.group.ui.main;
+
+import org.olat.core.gui.UserRequest;
+import org.olat.core.gui.components.form.flexible.FormItemContainer;
+import org.olat.core.gui.components.form.flexible.elements.MultipleSelectionElement;
+import org.olat.core.gui.components.form.flexible.impl.FormBasicController;
+import org.olat.core.gui.components.form.flexible.impl.FormLayoutContainer;
+import org.olat.core.gui.control.Controller;
+import org.olat.core.gui.control.Event;
+import org.olat.core.gui.control.WindowControl;
+
+
+/**
+ * 
+ * Initial date: 27.11.2012<br>
+ * @author srosse, stephane.rosse@frentix.com, http://www.frentix.com
+ *
+ */
+public class DedupMembersConfirmationController extends FormBasicController {
+	
+	private static final String[] keys = { "coaches", "participants" };
+	
+	private MultipleSelectionElement typEl;
+
+	public DedupMembersConfirmationController(UserRequest ureq, WindowControl wControl) {
+		super(ureq, wControl, "dedup");
+		
+		initForm(ureq);
+	}
+
+	@Override
+	protected void initForm(FormItemContainer formLayout, Controller listener, UserRequest ureq) {
+		FormLayoutContainer optionsCont = FormLayoutContainer.createDefaultFormLayout("options", getTranslator());
+		formLayout.add(optionsCont);
+		formLayout.add("options", optionsCont);
+		String[] values = new String[] {
+				translate("dedup.members.coaches"), translate("dedup.members.particpants")
+		};
+		typEl = uifactory.addCheckboxesVertical("typ", "dedup.members.typ", optionsCont, keys, values, null, 1);
+		typEl.select(keys[0], true);
+		typEl.select(keys[1], true);
+
+		FormLayoutContainer buttonCont = FormLayoutContainer.createButtonLayout("buttons", getTranslator());
+		formLayout.add(buttonCont);
+		formLayout.add("buttons", buttonCont);
+		uifactory.addFormSubmitButton("ok", buttonCont);
+		uifactory.addFormCancelButton("cancel", buttonCont, ureq, getWindowControl());
+	}
+	
+	public boolean isDedupCoaches() {
+		return typEl.isSelected(0);
+	}
+	
+	public boolean isDedupParticipants() {
+		return typEl.isSelected(1);
+	}
+	
+	@Override
+	protected void doDispose() {
+		//
+	}
+
+	@Override
+	protected void formOK(UserRequest ureq) {
+		fireEvent(ureq, Event.DONE_EVENT);
+	}
+
+	@Override
+	protected void formCancelled(UserRequest ureq) {
+		fireEvent(ureq, Event.CANCELLED_EVENT);
+	}
+}
\ No newline at end of file
diff --git a/src/main/java/org/olat/group/ui/main/_content/dedup.html b/src/main/java/org/olat/group/ui/main/_content/dedup.html
new file mode 100644
index 00000000000..8faa6c97bb4
--- /dev/null
+++ b/src/main/java/org/olat/group/ui/main/_content/dedup.html
@@ -0,0 +1,3 @@
+$r.translate("dedup.members.info")
+$r.render("options")
+$r.render("buttons")
diff --git a/src/main/java/org/olat/group/ui/main/_i18n/LocalStrings_de.properties b/src/main/java/org/olat/group/ui/main/_i18n/LocalStrings_de.properties
index a0506470946..9c09c71ccf0 100644
--- a/src/main/java/org/olat/group/ui/main/_i18n/LocalStrings_de.properties
+++ b/src/main/java/org/olat/group/ui/main/_i18n/LocalStrings_de.properties
@@ -3,6 +3,11 @@ create.form.title=Neue Gruppe erstellen
 create.group=Gruppe erstellen
 create.group.description=Erzeugen Sie eine neue Gruppe mit der unten stehenden Schaltfläche. Als Betreuer dieser Gruppe können Sie danach die Gruppenwerkzeuge freischalten, Benutzer hinzufügen oder die Gruppe veröffentlichen.
 copy.group=Kopieren
+deup.members=Mitglieder deduplizieren
+dedup.members.typ=Typ
+dedup.members.coaches=Betreuer
+dedup.members.particpants=Teilnehmer
+dedup.members.info=Wollen Sie die Mitglieder deduplizieren?
 dialog.modal.bg.delete.title=Gruppe l\u00F6schen?
 dialog.modal.bg.delete.text=Wollen Sie die Gruppe "{0}" wirklich l\u00F6schen?
 dialog.modal.bg.mail.text=Wollen Sie die Mitglieder per Mail benachrichtigen?
diff --git a/src/main/java/org/olat/repository/RepositoryManager.java b/src/main/java/org/olat/repository/RepositoryManager.java
index 46312771f15..f972b7bdc83 100644
--- a/src/main/java/org/olat/repository/RepositoryManager.java
+++ b/src/main/java/org/olat/repository/RepositoryManager.java
@@ -32,7 +32,6 @@ import java.util.Collections;
 import java.util.Date;
 import java.util.List;
 
-import javax.persistence.EntityManager;
 import javax.persistence.LockModeType;
 import javax.persistence.TypedQuery;
 
@@ -91,7 +90,6 @@ import org.olat.resource.OLATResource;
 import org.olat.resource.OLATResourceImpl;
 import org.olat.resource.OLATResourceManager;
 import org.olat.util.logging.activity.LoggingResourceable;
-import org.springframework.transaction.annotation.Propagation;
 import org.springframework.transaction.annotation.Transactional;
 
 /**
diff --git a/src/main/java/org/olat/user/restapi/RolesVO.java b/src/main/java/org/olat/user/restapi/RolesVO.java
index b99369dd257..f1fcc853ee8 100644
--- a/src/main/java/org/olat/user/restapi/RolesVO.java
+++ b/src/main/java/org/olat/user/restapi/RolesVO.java
@@ -1,3 +1,22 @@
+/**
+ * <a href="http://www.openolat.org">
+ * OpenOLAT - Online Learning and Training</a><br>
+ * <p>
+ * Licensed under the Apache License, Version 2.0 (the "License"); <br>
+ * you may not use this file except in compliance with the License.<br>
+ * You may obtain a copy of the License at the
+ * <a href="http://www.apache.org/licenses/LICENSE-2.0">Apache homepage</a>
+ * <p>
+ * Unless required by applicable law or agreed to in writing,<br>
+ * software distributed under the License is distributed on an "AS IS" BASIS, <br>
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. <br>
+ * See the License for the specific language governing permissions and <br>
+ * limitations under the License.
+ * <p>
+ * Initial code contributed and copyrighted by<br>
+ * frentix GmbH, http://www.frentix.com
+ * <p>
+ */
 package org.olat.user.restapi;
 
 import org.olat.core.id.Roles;
diff --git a/src/test/java/org/olat/search/service/document/file/OfficeDocumentTest.java b/src/test/java/org/olat/search/service/document/file/OfficeDocumentTest.java
index 4a92bd3e9e9..d6f63e1de37 100644
--- a/src/test/java/org/olat/search/service/document/file/OfficeDocumentTest.java
+++ b/src/test/java/org/olat/search/service/document/file/OfficeDocumentTest.java
@@ -1,3 +1,22 @@
+/**
+ * <a href="http://www.openolat.org">
+ * OpenOLAT - Online Learning and Training</a><br>
+ * <p>
+ * Licensed under the Apache License, Version 2.0 (the "License"); <br>
+ * you may not use this file except in compliance with the License.<br>
+ * You may obtain a copy of the License at the
+ * <a href="http://www.apache.org/licenses/LICENSE-2.0">Apache homepage</a>
+ * <p>
+ * Unless required by applicable law or agreed to in writing,<br>
+ * software distributed under the License is distributed on an "AS IS" BASIS, <br>
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. <br>
+ * See the License for the specific language governing permissions and <br>
+ * limitations under the License.
+ * <p>
+ * Initial code contributed and copyrighted by<br>
+ * frentix GmbH, http://www.frentix.com
+ * <p>
+ */
 package org.olat.search.service.document.file;
 
 import java.io.File;
-- 
GitLab