diff --git a/src/main/java/org/olat/core/gui/media/ServletUtil.java b/src/main/java/org/olat/core/gui/media/ServletUtil.java
index 0b5dc734cb44c833f5e26d5c790b11625df6d96a..f7bc631ee1b12c44ecf3a4c3b12bd507edeb266b 100644
--- a/src/main/java/org/olat/core/gui/media/ServletUtil.java
+++ b/src/main/java/org/olat/core/gui/media/ServletUtil.java
@@ -230,17 +230,23 @@ public class ServletUtil {
 			}
 		} catch (IOException e) {
 			FileUtils.closeSafely(out);
+			handleIOException("client browser probably abort when serving media resource", e);
+		} finally {
+			IOUtils.closeQuietly(bis);
+			IOUtils.closeQuietly(in);
+		}
+	}
+	
+	public static final void handleIOException(String msg, Exception e) {
+		try {
 			String className = e.getClass().getSimpleName();
 			if("ClientAbortException".equals(className)) {
-				if(log.isDebugEnabled()) {//video generate a lot of these errors
-					log.warn("client browser probably abort when serving media resource", e);
-				}
+				log.debug("client browser probably abort during operaation", e);
 			} else {
-				log.error("client browser probably abort when serving media resource", e);
+				log.error(msg, e);
 			}
-		} finally {
-			IOUtils.closeQuietly(bis);
-			IOUtils.closeQuietly(in);
+		} catch (Exception e1) {
+			log.error("", e1);
 		}
 	}
 	
diff --git a/src/main/java/org/olat/core/servlets/StaticServlet.java b/src/main/java/org/olat/core/servlets/StaticServlet.java
index e18edb5439ae17a473bbcad19c62650749039c49..43b77cdba4a6a288c24c05907ad3ecf8872c75a5 100644
--- a/src/main/java/org/olat/core/servlets/StaticServlet.java
+++ b/src/main/java/org/olat/core/servlets/StaticServlet.java
@@ -205,6 +205,8 @@ public class StaticServlet extends HttpServlet {
 			try(InputStream in = new FileInputStream(file);
 					BufferedInputStream bis = new BufferedInputStream(in, FileUtils.BSIZE)) {
 				FileUtils.cpio(bis, response.getOutputStream(), "static");
+			} catch(IOException e) {
+				ServletUtil.handleIOException("", e);
 			} catch(Exception ex) {
 				log.error("", ex);
 			}
diff --git a/src/main/java/org/olat/modules/bigbluebutton/BigBlueButtonManager.java b/src/main/java/org/olat/modules/bigbluebutton/BigBlueButtonManager.java
index 429f3fae1eb07117e9c846d8c0ebc281f43ce0fc..8e18ffd750a888d19168138be3ae27d6bc5ca560 100644
--- a/src/main/java/org/olat/modules/bigbluebutton/BigBlueButtonManager.java
+++ b/src/main/java/org/olat/modules/bigbluebutton/BigBlueButtonManager.java
@@ -37,6 +37,17 @@ import org.olat.repository.RepositoryEntryRef;
  */
 public interface BigBlueButtonManager {
 	
+	public BigBlueButtonServer createServer(String url, String recordingUrl, String sharedSecret);
+	
+	public BigBlueButtonServer updateServer(BigBlueButtonServer server);
+	
+	public boolean hasServer(String url);
+	
+	public List<BigBlueButtonServer> getServers();
+	
+	public void deleteServer(BigBlueButtonServer server, BigBlueButtonErrors errors);
+	
+	
 	/**
 	 * Create and persist a meeting in OpenOlat. The method will generate
 	 * an unique meeting identifier and passwords for attendees and moderators.
diff --git a/src/main/java/org/olat/modules/bigbluebutton/BigBlueButtonMeeting.java b/src/main/java/org/olat/modules/bigbluebutton/BigBlueButtonMeeting.java
index a5cf5a36af378b54dfe370062cd73749bac82332..0534275e6e6d9a9e68dff632d36c4709a95f18dc 100644
--- a/src/main/java/org/olat/modules/bigbluebutton/BigBlueButtonMeeting.java
+++ b/src/main/java/org/olat/modules/bigbluebutton/BigBlueButtonMeeting.java
@@ -87,4 +87,6 @@ public interface BigBlueButtonMeeting extends ModifiedInfo, CreateInfo {
 	public RepositoryEntry getEntry();
 
 	public String getSubIdent();
+	
+	public BigBlueButtonServer getServer();
 }
diff --git a/src/main/java/org/olat/modules/bigbluebutton/BigBlueButtonServer.java b/src/main/java/org/olat/modules/bigbluebutton/BigBlueButtonServer.java
new file mode 100644
index 0000000000000000000000000000000000000000..562d600f9b452e9189926962dfdfa52669540905
--- /dev/null
+++ b/src/main/java/org/olat/modules/bigbluebutton/BigBlueButtonServer.java
@@ -0,0 +1,59 @@
+/**
+ * <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.modules.bigbluebutton;
+
+import org.olat.core.id.CreateInfo;
+import org.olat.core.id.ModifiedInfo;
+
+/**
+ * 
+ * Initial date: 7 avr. 2020<br>
+ * @author srosse, stephane.rosse@frentix.com, http://www.frentix.com
+ *
+ */
+public interface BigBlueButtonServer extends ModifiedInfo, CreateInfo {
+	
+	public Long getKey();
+	
+	public String getName();
+
+	public void setName(String name);
+
+	public String getUrl();
+
+	public void setUrl(String url);
+
+	public String getSharedSecret();
+
+	public void setSharedSecret(String secret);
+
+	public String getRecordingUrl();
+
+	public void setRecordingUrl(String recordingUrl);
+
+	public boolean isEnabled();
+
+	public void setEnabled(boolean enabled);
+	
+	public Double getCapacityFactory();
+
+	public void setCapacityFactory(Double capacityFactory);
+
+}
diff --git a/src/main/java/org/olat/modules/bigbluebutton/manager/BigBlueButtonManagerImpl.java b/src/main/java/org/olat/modules/bigbluebutton/manager/BigBlueButtonManagerImpl.java
index 29baa187fddec273aabe505def9c824a15713978..bfda0743bb68bdd8106563c30aa1df008c965b67 100644
--- a/src/main/java/org/olat/modules/bigbluebutton/manager/BigBlueButtonManagerImpl.java
+++ b/src/main/java/org/olat/modules/bigbluebutton/manager/BigBlueButtonManagerImpl.java
@@ -24,6 +24,9 @@ import java.util.ArrayList;
 import java.util.Collections;
 import java.util.Date;
 import java.util.List;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.stream.Collectors;
 
 import org.apache.http.client.methods.CloseableHttpResponse;
 import org.apache.http.client.methods.HttpGet;
@@ -35,6 +38,7 @@ import org.olat.commons.calendar.CalendarManager;
 import org.olat.commons.calendar.model.Kalendar;
 import org.olat.commons.calendar.model.KalendarEvent;
 import org.olat.commons.calendar.ui.components.KalendarRenderWrapper;
+import org.olat.core.commons.persistence.DB;
 import org.olat.core.id.Identity;
 import org.olat.core.id.Roles;
 import org.olat.core.id.User;
@@ -50,13 +54,15 @@ import org.olat.group.BusinessGroupService;
 import org.olat.modules.bigbluebutton.BigBlueButtonManager;
 import org.olat.modules.bigbluebutton.BigBlueButtonMeeting;
 import org.olat.modules.bigbluebutton.BigBlueButtonMeetingTemplate;
-import org.olat.modules.bigbluebutton.BigBlueButtonModule;
 import org.olat.modules.bigbluebutton.BigBlueButtonRecording;
+import org.olat.modules.bigbluebutton.BigBlueButtonServer;
 import org.olat.modules.bigbluebutton.BigBlueButtonTemplatePermissions;
 import org.olat.modules.bigbluebutton.GuestPolicyEnum;
 import org.olat.modules.bigbluebutton.model.BigBlueButtonError;
 import org.olat.modules.bigbluebutton.model.BigBlueButtonErrorCodes;
 import org.olat.modules.bigbluebutton.model.BigBlueButtonErrors;
+import org.olat.modules.bigbluebutton.model.BigBlueButtonMeetingImpl;
+import org.olat.modules.bigbluebutton.model.BigBlueButtonMeetingInfos;
 import org.olat.repository.RepositoryEntry;
 import org.olat.repository.RepositoryEntryRef;
 import org.olat.repository.RepositoryManager;
@@ -78,6 +84,8 @@ public class BigBlueButtonManagerImpl implements BigBlueButtonManager, Initializ
 	
 	private static final Logger log = Tracing.createLoggerFor(BigBlueButtonManagerImpl.class);
 
+	@Autowired
+	private DB dbInstance;
 	@Autowired
 	private CalendarManager calendarManager;
 	@Autowired
@@ -85,10 +93,10 @@ public class BigBlueButtonManagerImpl implements BigBlueButtonManager, Initializ
 	@Autowired
 	private RepositoryEntryDAO repositoryEntryDao;
 	@Autowired
-	private BigBlueButtonModule bigBlueButtonModule;
-	@Autowired
 	private BusinessGroupService businessGroupService;
 	@Autowired
+	private BigBlueButtonServerDAO bigBlueButtonServerDao;
+	@Autowired
 	private BigBlueButtonMeetingDAO bigBlueButtonMeetingDao;
 	@Autowired
 	private BigBlueButtonMeetingTemplateDAO bigBlueButtonMeetingTemplateDao;
@@ -202,6 +210,36 @@ public class BigBlueButtonManagerImpl implements BigBlueButtonManager, Initializ
 		bigBlueButtonMeetingTemplateDao.updateTemplate(template);
 	}
 
+	@Override
+	public BigBlueButtonServer createServer(String url, String recordingUrl, String sharedSecret) {
+		return bigBlueButtonServerDao.createServer(url, recordingUrl, sharedSecret);
+	}
+
+	@Override
+	public BigBlueButtonServer updateServer(BigBlueButtonServer server) {
+		return bigBlueButtonServerDao.updateServer(server);
+	}
+
+	@Override
+	public List<BigBlueButtonServer> getServers() {
+		return bigBlueButtonServerDao.getServers();
+	}
+
+	@Override
+	public void deleteServer(BigBlueButtonServer server, BigBlueButtonErrors errors) {
+		List<BigBlueButtonMeeting> meetings = bigBlueButtonMeetingDao.getMeetings(server);
+		for(BigBlueButtonMeeting meeting:meetings) {
+			deleteMeeting(meeting, errors);
+		}
+		bigBlueButtonServerDao.deleteServer(server);
+	}
+
+	@Override
+	public boolean hasServer(String url) {
+		List<BigBlueButtonServer> servers = this.getServers();
+		return servers.stream().anyMatch(server -> server.getUrl().startsWith(url) || url.startsWith(server.getUrl()));
+	}
+
 	@Override
 	public BigBlueButtonMeeting createAndPersistMeeting(String name, RepositoryEntry entry, String subIdent, BusinessGroup businessGroup) {
 		return bigBlueButtonMeetingDao.createAndPersistMeeting(name, entry, subIdent, businessGroup);
@@ -295,6 +333,77 @@ public class BigBlueButtonManagerImpl implements BigBlueButtonManager, Initializ
 		return false;
 	}
 	
+	public BigBlueButtonServer getAvailableServer() {
+		List<BigBlueButtonServer> servers = getServers();
+		List<BigBlueButtonServer> availableServers = servers.stream()
+				.filter(BigBlueButtonServer::isEnabled)
+				.collect(Collectors.toList());
+		if(availableServers.isEmpty()) {
+			return null;
+		} else if(availableServers.size() == 1) {//TODO 
+			return availableServers.get(0);
+		}
+		return getBigBlueButtonServer(servers);
+	}
+	
+	private BigBlueButtonServer getBigBlueButtonServer(List<BigBlueButtonServer> servers) {
+		CountDownLatch serverLatch = new CountDownLatch(servers.size());
+		
+		List<MeetingInfosThread> threads = new ArrayList<>();
+		for(BigBlueButtonServer server:servers) {
+			MeetingInfosThread thread = new MeetingInfosThread(server, serverLatch);
+			threads.add(thread);
+			thread.start();
+		}
+		
+		try {
+			serverLatch.await(15, TimeUnit.SECONDS);
+		} catch (InterruptedException e) {
+			log.error("", e);
+		}
+		
+		List<ServerLoad> serversLoad = threads.stream()
+				.filter(MeetingInfosThread::isExecuted)
+				.filter(thread -> !thread.hasErrors())
+				.map(thread -> calculateLoad(thread.getServer(), thread.getMeetingsInfos()))
+				.collect(Collectors.toList());
+		
+		if(serversLoad.isEmpty()) {
+			return null;
+		}
+		Collections.sort(serversLoad);
+		return serversLoad.get(0).getServer();
+	}
+	
+	private ServerLoad calculateLoad(BigBlueButtonServer server, List<BigBlueButtonMeetingInfos> meetingsInfos) {
+		double load = 0.0d;
+		for(BigBlueButtonMeetingInfos meetingInfos:meetingsInfos) {
+			load += meetingInfos.getListenerCount() * 1.0d;
+			load += meetingInfos.getVideoCount() * 3.0d;
+			load += meetingInfos.getVoiceParticipantCount() * 2.0d;
+		}
+		if(load > 0.0d
+				&& server.getCapacityFactory() != null
+				&& server.getCapacityFactory().doubleValue() > 1.0) {
+			load = load / server.getCapacityFactory().doubleValue();
+		}
+		return new ServerLoad(server, load);
+	}
+	
+	private List<BigBlueButtonMeetingInfos> getMeetingInfos(BigBlueButtonServer server, BigBlueButtonErrors errors) {
+		BigBlueButtonUriBuilder uriBuilder = getUriBuilder(server);
+		uriBuilder
+			.operation("getMeetings");
+		
+		Document doc = sendRequest(uriBuilder, errors);
+		BigBlueButtonUtils.print(doc);
+		if(BigBlueButtonUtils.checkSuccess(doc, errors)) {
+			BigBlueButtonUtils.print(doc);
+			return BigBlueButtonUtils.getMeetings(doc);
+		}
+		return new ArrayList<>();
+	}
+	
 	private void deleteRecordings(BigBlueButtonMeeting meeting, BigBlueButtonErrors errors) {
 		StringBuilder sb = new StringBuilder();
 		
@@ -310,12 +419,12 @@ public class BigBlueButtonManagerImpl implements BigBlueButtonManager, Initializ
 		}
 		
 		if(sb.length() > 0) {
-			deleteRecording(sb.toString(), errors);
+			deleteRecording(sb.toString(), meeting.getServer(), errors);
 		}
 	}
 	
-	private void deleteRecording(String recordId, BigBlueButtonErrors errors) {
-		BigBlueButtonUriBuilder uriBuilder = getUriBuilder();
+	private void deleteRecording(String recordId, BigBlueButtonServer server, BigBlueButtonErrors errors) {
+		BigBlueButtonUriBuilder uriBuilder = getUriBuilder(server);
 		uriBuilder
 			.operation("deleteRecordings")
 			.parameter("recordID", recordId);
@@ -422,7 +531,12 @@ public class BigBlueButtonManagerImpl implements BigBlueButtonManager, Initializ
 
 	@Override
 	public boolean isMeetingRunning(BigBlueButtonMeeting meeting) {
-		BigBlueButtonUriBuilder uriBuilder = getUriBuilder();
+		BigBlueButtonServer server = meeting.getServer();
+		if(server == null) {
+			return false;
+		}
+		
+		BigBlueButtonUriBuilder uriBuilder = getUriBuilder(server);
 		uriBuilder
 			.operation("isMeetingRunning")
 			.parameter("meetingID", meeting.getMeetingId());
@@ -438,14 +552,21 @@ public class BigBlueButtonManagerImpl implements BigBlueButtonManager, Initializ
 
 	@Override
 	public String join(BigBlueButtonMeeting meeting, Identity identity, boolean moderator, boolean guest, Boolean isRunning, BigBlueButtonErrors errors) {
+		this.getAvailableServer();
+		
 		String joinUrl = null;
-		if((isRunning != null && isRunning.booleanValue()) || createBigBlueButtonMeeting(meeting, errors)) {
-			joinUrl = buildJoinUrl(meeting, identity, moderator, guest);
+		if(isRunning != null && isRunning.booleanValue() && meeting.getServer() != null) {
+			joinUrl = buildJoinUrl(meeting, meeting.getServer(), identity, moderator, guest);
+		} else {
+			meeting = getMeetingWithServer(meeting);
+			if(createBigBlueButtonMeeting(meeting, errors)) {
+				joinUrl = buildJoinUrl(meeting, meeting.getServer(), identity, moderator, guest);
+			}
 		}
 		return joinUrl;
 	}
 	
-	private String buildJoinUrl(BigBlueButtonMeeting meeting, Identity identity, boolean moderator, boolean guest) {
+	private String buildJoinUrl(BigBlueButtonMeeting meeting, BigBlueButtonServer server, Identity identity, boolean moderator, boolean guest) {
 		String password = moderator ? meeting.getModeratorPassword() : meeting.getAttendeePassword();
 		
 		String userId = null;
@@ -453,7 +574,7 @@ public class BigBlueButtonManagerImpl implements BigBlueButtonManager, Initializ
 			userId = WebappHelper.getInstanceId() + "-" + identity.getKey();
 		}
 
-		BigBlueButtonUriBuilder uriBuilder = getUriBuilder();
+		BigBlueButtonUriBuilder uriBuilder = getUriBuilder(server);
 		return uriBuilder
 			.operation("join")
 			.parameter("meetingID", meeting.getMeetingId())
@@ -491,11 +612,35 @@ public class BigBlueButtonManagerImpl implements BigBlueButtonManager, Initializ
 		}
 		return BusinessControlFactory.getInstance().getURLFromBusinessPathString(businessPath);
 	}
+	
+	private BigBlueButtonMeeting getMeetingWithServer(BigBlueButtonMeeting meeting) {
+		meeting = bigBlueButtonMeetingDao.loadByKey(meeting.getKey());
+		if(meeting.getServer() != null) {
+			return meeting;
+		}
+		
+		BigBlueButtonServer availableServer = getAvailableServer();
+		if(availableServer == null) {
+			return meeting;
+		}
+		
+		BigBlueButtonMeeting lockedMeeting = bigBlueButtonMeetingDao.loadForUpdate(meeting);
+		BigBlueButtonServer currentServer = lockedMeeting.getServer();
+		if(currentServer == null) {
+			((BigBlueButtonMeetingImpl)lockedMeeting).setServer(availableServer);
+			meeting = bigBlueButtonMeetingDao.updateMeeting(lockedMeeting);
+			meeting.getServer().getUrl();// ensure server is loaded
+		}
+		dbInstance.commit();
+		
+		return meeting;
+	}
 
 	private boolean createBigBlueButtonMeeting(BigBlueButtonMeeting meeting, BigBlueButtonErrors errors) {
 		BigBlueButtonMeetingTemplate template = meeting.getTemplate();
+		BigBlueButtonServer server = meeting.getServer();
 		
-		BigBlueButtonUriBuilder uriBuilder = getUriBuilder();
+		BigBlueButtonUriBuilder uriBuilder = getUriBuilder(server);
 		uriBuilder
 			.operation("create")
 			.optionalParameter("name", meeting.getName())
@@ -545,7 +690,11 @@ public class BigBlueButtonManagerImpl implements BigBlueButtonManager, Initializ
 
 	@Override
 	public List<BigBlueButtonRecording> getRecordings(BigBlueButtonMeeting meeting, BigBlueButtonErrors errors) {
-		BigBlueButtonUriBuilder uriBuilder = getUriBuilder();
+		if(meeting.getServer() == null) {
+			return new ArrayList<>();
+		}
+		
+		BigBlueButtonUriBuilder uriBuilder = getUriBuilder(meeting.getServer());
 		uriBuilder
 			.operation("getRecordings")
 			.parameter("meetingID", meeting.getMeetingId());
@@ -558,8 +707,8 @@ public class BigBlueButtonManagerImpl implements BigBlueButtonManager, Initializ
 		return Collections.emptyList();
 	}
 	
-	public void getBigBlueButtonDefaultConfigXml() {
-		BigBlueButtonUriBuilder uriBuilder = getUriBuilder();
+	public void getBigBlueButtonDefaultConfigXml(BigBlueButtonServer server) {
+		BigBlueButtonUriBuilder uriBuilder = getUriBuilder(server);
 		uriBuilder
 			.operation("getDefaultConfigXML");
 		BigBlueButtonErrors errors = new BigBlueButtonErrors();
@@ -579,8 +728,9 @@ public class BigBlueButtonManagerImpl implements BigBlueButtonManager, Initializ
 		return false;
 	}
 	
-	private BigBlueButtonUriBuilder getUriBuilder() {
-		return BigBlueButtonUriBuilder.fromUri(bigBlueButtonModule.getBigBlueButtonURI(), bigBlueButtonModule.getSharedSecret());	
+	private BigBlueButtonUriBuilder getUriBuilder(BigBlueButtonServer server) {
+		URI uri = URI.create(server.getUrl());
+		return BigBlueButtonUriBuilder.fromUri(uri, server.getSharedSecret());	
 	}
 	
 	protected Document sendRequest(BigBlueButtonUriBuilder builder, BigBlueButtonErrors errors) {
@@ -597,4 +747,68 @@ public class BigBlueButtonManagerImpl implements BigBlueButtonManager, Initializ
 			return null;
 		}
 	}
+	
+	private static class ServerLoad implements Comparable<ServerLoad> {
+		
+		private final double load;
+		private final BigBlueButtonServer server;
+		
+		public ServerLoad(BigBlueButtonServer server, double load) {
+			this.server = server;
+			this.load = load;
+		}
+
+		public BigBlueButtonServer getServer() {
+			return server;
+		}
+
+		@Override
+		public int compareTo(ServerLoad o) {
+			return Double.compare(load, o.load);
+		}
+	}
+	
+	private class MeetingInfosThread extends Thread {
+		
+		private boolean executed = false;
+		private final CountDownLatch latch;
+		private final BigBlueButtonServer server;
+		private List<BigBlueButtonMeetingInfos> infos;
+		private final BigBlueButtonErrors errors = new BigBlueButtonErrors();
+		
+		public MeetingInfosThread(BigBlueButtonServer server, CountDownLatch latch) {
+			super("BBB-Meetings-Infos");
+			this.latch = latch;
+			this.server = server;
+			setDaemon(true);
+		}
+		
+		public boolean isExecuted() {
+			return executed;
+		}
+		
+		public boolean hasErrors() {
+			return errors.hasErrors();
+		}
+		
+		public List<BigBlueButtonMeetingInfos> getMeetingsInfos() {
+			return infos;
+		}
+		
+		public BigBlueButtonServer getServer() {
+			return server;
+		}
+
+		@Override
+		public void run() {
+			try {
+				infos = getMeetingInfos(server, errors);
+				executed = true;
+			} catch(Exception e) {
+				log.error("", e);
+			} finally {
+				latch.countDown();
+			}
+		}
+	}
 }
diff --git a/src/main/java/org/olat/modules/bigbluebutton/manager/BigBlueButtonMeetingDAO.java b/src/main/java/org/olat/modules/bigbluebutton/manager/BigBlueButtonMeetingDAO.java
index 8fff348429400daf1035515b496b7b013b559273..e23bea5fcc9e053b9f5e3787e237168cc4e7cd9d 100644
--- a/src/main/java/org/olat/modules/bigbluebutton/manager/BigBlueButtonMeetingDAO.java
+++ b/src/main/java/org/olat/modules/bigbluebutton/manager/BigBlueButtonMeetingDAO.java
@@ -24,6 +24,7 @@ import java.util.Date;
 import java.util.List;
 import java.util.UUID;
 
+import javax.persistence.LockModeType;
 import javax.persistence.TypedQuery;
 
 import org.olat.core.commons.persistence.DB;
@@ -32,6 +33,7 @@ import org.olat.core.util.StringHelper;
 import org.olat.group.BusinessGroup;
 import org.olat.modules.bigbluebutton.BigBlueButtonMeeting;
 import org.olat.modules.bigbluebutton.BigBlueButtonMeetingTemplate;
+import org.olat.modules.bigbluebutton.BigBlueButtonServer;
 import org.olat.modules.bigbluebutton.model.BigBlueButtonMeetingImpl;
 import org.olat.repository.RepositoryEntry;
 import org.olat.repository.RepositoryEntryRef;
@@ -74,6 +76,7 @@ public class BigBlueButtonMeetingDAO {
 		  .append(" left join fetch meeting.entry as entry")
 		  .append(" left join fetch meeting.businessGroup as businessGroup")
 		  .append(" left join fetch meeting.template as template")
+		  .append(" left join fetch meeting.server as server")
 		  .append(" where meeting.key=:meetingKey");
 		
 		List<BigBlueButtonMeeting> meetings = dbInstance.getCurrentEntityManager()
@@ -83,6 +86,37 @@ public class BigBlueButtonMeetingDAO {
 		return meetings == null || meetings.isEmpty() ? null : meetings.get(0);
 	}
 	
+	public BigBlueButtonMeeting loadForUpdate(BigBlueButtonMeeting meeting) {
+		//first remove it from caches
+		dbInstance.getCurrentEntityManager().detach(meeting);
+
+		StringBuilder sb = new StringBuilder();
+		sb.append("select meeting from bigbluebuttonmeeting as meeting")
+		  .append(" where meeting.key=:meetingKey");
+
+		List<BigBlueButtonMeeting> meetings = dbInstance.getCurrentEntityManager()
+				.createQuery(sb.toString(), BigBlueButtonMeeting.class)
+				.setParameter("meetingKey", meeting.getKey())
+				.setLockMode(LockModeType.PESSIMISTIC_WRITE)
+				.getResultList();
+		return meetings == null || meetings.isEmpty() ? null : meetings.get(0);
+	}
+	
+	public List<BigBlueButtonMeeting> getMeetings(BigBlueButtonServer server) {
+		QueryBuilder sb = new QueryBuilder();
+		sb.append("select meeting from bigbluebuttonmeeting as meeting")
+		  .append(" left join fetch meeting.entry as entry")
+		  .append(" left join fetch meeting.businessGroup as businessGroup")
+		  .append(" left join fetch meeting.template as template")
+		  .append(" left join fetch meeting.server as server")
+		  .append(" where meeting.server.key=:serverKey");
+		
+		return dbInstance.getCurrentEntityManager()
+				.createQuery(sb.toString(), BigBlueButtonMeeting.class)
+				.setParameter("serverKey", server.getKey())
+				.getResultList();
+	}
+	
 	public BigBlueButtonMeeting updateMeeting(BigBlueButtonMeeting meeting) {
 		meeting.setLastModified(new Date());
 		updateDates((BigBlueButtonMeetingImpl)meeting,
@@ -155,7 +189,8 @@ public class BigBlueButtonMeetingDAO {
 		sb.append("select meeting from bigbluebuttonmeeting as meeting")
 		  .append(" left join fetch meeting.entry as entry")
 		  .append(" left join fetch meeting.businessGroup as businessGroup")
-		  .append(" left join fetch meeting.template as template");
+		  .append(" left join fetch meeting.template as template")
+		  .append(" left join fetch meeting.server as server");
 		return dbInstance.getCurrentEntityManager()
 				.createQuery(sb.toString(), BigBlueButtonMeeting.class)
 				.getResultList();
@@ -188,7 +223,8 @@ public class BigBlueButtonMeetingDAO {
 	public List<BigBlueButtonMeeting> getMeetings(RepositoryEntryRef entry, String subIdent, BusinessGroup businessGroup) {
 		QueryBuilder sb = new QueryBuilder();
 		sb.append("select meeting from bigbluebuttonmeeting as meeting")
-		  .append(" left join fetch meeting.template as template");
+		  .append(" left join fetch meeting.template as template")
+		  .append(" left join fetch meeting.server as server");
 		if(entry != null) {
 			sb.and().append("meeting.entry.key=:entryKey");
 			if(StringHelper.containsNonWhitespace(subIdent)) {
@@ -218,6 +254,7 @@ public class BigBlueButtonMeetingDAO {
 		QueryBuilder sb = new QueryBuilder();
 		sb.append("select meeting from bigbluebuttonmeeting as meeting")
 		  .append(" left join fetch meeting.template as template")
+		  .append(" left join fetch meeting.server as server")
 		  .append(" where meeting.entry.key=:entryKey and meeting.permanent=false")
 		  .append(" and meeting.startDate is not null and meeting.endDate is not null");
 		if(StringHelper.containsNonWhitespace(subIdent)) {
diff --git a/src/main/java/org/olat/modules/bigbluebutton/manager/BigBlueButtonServerDAO.java b/src/main/java/org/olat/modules/bigbluebutton/manager/BigBlueButtonServerDAO.java
new file mode 100644
index 0000000000000000000000000000000000000000..86a83382dd06b984814da9d2fa6bd61944e1bf5c
--- /dev/null
+++ b/src/main/java/org/olat/modules/bigbluebutton/manager/BigBlueButtonServerDAO.java
@@ -0,0 +1,74 @@
+/**
+ * <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.modules.bigbluebutton.manager;
+
+import java.util.Date;
+import java.util.List;
+
+import org.olat.core.commons.persistence.DB;
+import org.olat.modules.bigbluebutton.BigBlueButtonServer;
+import org.olat.modules.bigbluebutton.model.BigBlueButtonServerImpl;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+/**
+ * 
+ * Initial date: 7 avr. 2020<br>
+ * @author srosse, stephane.rosse@frentix.com, http://www.frentix.com
+ *
+ */
+@Service
+public class BigBlueButtonServerDAO {
+	
+	@Autowired
+	private DB dbInstance;
+	
+	public BigBlueButtonServer createServer(String url, String recordingUrl, String sharedSecret) {
+		BigBlueButtonServerImpl server = new BigBlueButtonServerImpl();
+		server.setCreationDate(new Date());
+		server.setLastModified(server.getCreationDate());
+		server.setUrl(url);
+		server.setRecordingUrl(recordingUrl);
+		server.setSharedSecret(sharedSecret);
+		server.setEnabled(true);
+		server.setCapacityFactory(1.0d);
+		dbInstance.getCurrentEntityManager().persist(server);
+		return server;
+	}
+	
+	public BigBlueButtonServer updateServer(BigBlueButtonServer server) {
+		((BigBlueButtonServerImpl)server).setLastModified(new Date());
+		return dbInstance.getCurrentEntityManager().merge(server);
+	}
+	
+	public void deleteServer(BigBlueButtonServer server) {
+		BigBlueButtonServer reloadedServer = dbInstance.getCurrentEntityManager()
+				.getReference(BigBlueButtonServerImpl.class, server.getKey());
+		dbInstance.getCurrentEntityManager().remove(reloadedServer);
+	}
+	
+	public List<BigBlueButtonServer> getServers() {
+		String q = "select server from bigbluebuttonserver server";
+		return dbInstance.getCurrentEntityManager()
+				.createQuery(q, BigBlueButtonServer.class)
+				.getResultList();
+	}
+
+}
diff --git a/src/main/java/org/olat/modules/bigbluebutton/manager/BigBlueButtonUtils.java b/src/main/java/org/olat/modules/bigbluebutton/manager/BigBlueButtonUtils.java
index 7535be26ba21d538b6c204993b4cf72b64a3c938..9c5cb30fee6654348c4cd8a677a4b90afa414886 100644
--- a/src/main/java/org/olat/modules/bigbluebutton/manager/BigBlueButtonUtils.java
+++ b/src/main/java/org/olat/modules/bigbluebutton/manager/BigBlueButtonUtils.java
@@ -43,6 +43,7 @@ import org.olat.core.util.StringHelper;
 import org.olat.modules.bigbluebutton.BigBlueButtonRecording;
 import org.olat.modules.bigbluebutton.model.BigBlueButtonError;
 import org.olat.modules.bigbluebutton.model.BigBlueButtonErrors;
+import org.olat.modules.bigbluebutton.model.BigBlueButtonMeetingInfos;
 import org.olat.modules.bigbluebutton.model.BigBlueButtonRecordingImpl;
 import org.w3c.dom.CharacterData;
 import org.w3c.dom.Document;
@@ -127,6 +128,43 @@ public class BigBlueButtonUtils {
     	return recordings;
     }
     
+
+    protected static List<BigBlueButtonMeetingInfos> getMeetings(Document document) {
+    	List<BigBlueButtonMeetingInfos> meetings = new ArrayList<>();
+    	NodeList meetingList = document.getElementsByTagName("meeting");
+    	for(int i=meetingList.getLength(); i-->0; ) {
+    		Element meetingEl = (Element)meetingList.item(i);
+    		String meetingId = getFirstElementValue(meetingEl, "meetingID");
+    		BigBlueButtonMeetingInfos meeting = new BigBlueButtonMeetingInfos(meetingId);
+    		meetings.add(meeting);
+    		
+    		String videoCount = getFirstElementValue(meetingEl, "videoCount");
+    		meeting.setVideoCount(toLong(videoCount));
+    		String listenerCount = getFirstElementValue(meetingEl, "listenerCount");
+    		meeting.setListenerCount(toLong(listenerCount));
+    		String voiceParticipantCount = getFirstElementValue(meetingEl, "voiceParticipantCount");
+    		meeting.setVoiceParticipantCount(toLong(voiceParticipantCount));
+    		
+    		String participantCount = getFirstElementValue(meetingEl, "participantCount");
+    		meeting.setParticipantCount(toLong(participantCount));
+    		String moderatorCount = getFirstElementValue(meetingEl, "moderatorCount");
+    		meeting.setModeratorCount(toLong(moderatorCount));
+    	}
+    	return meetings;
+    }
+    
+    private static long toLong(String text) {
+    	if(StringHelper.isLong(text)) {
+    		try {
+				return Long.parseLong(text);
+			} catch (NumberFormatException e) {
+				log.error("Cannot parse this long: {0}", text, e);
+			}
+    		
+    	}
+    	return 0l;
+    }
+    
     private static Date toDate(String val) {
     	if(StringHelper.isLong(val)) {
     		Long time = Long.parseLong(val);
diff --git a/src/main/java/org/olat/modules/bigbluebutton/model/BigBlueButtonMeetingImpl.java b/src/main/java/org/olat/modules/bigbluebutton/model/BigBlueButtonMeetingImpl.java
index bf3266f32bb9c9535fda943f9a10e7dc990d4f55..146482c3d057df5a6e9a6a54c9dabe69e082b7ec 100644
--- a/src/main/java/org/olat/modules/bigbluebutton/model/BigBlueButtonMeetingImpl.java
+++ b/src/main/java/org/olat/modules/bigbluebutton/model/BigBlueButtonMeetingImpl.java
@@ -38,6 +38,7 @@ import org.olat.group.BusinessGroup;
 import org.olat.group.BusinessGroupImpl;
 import org.olat.modules.bigbluebutton.BigBlueButtonMeeting;
 import org.olat.modules.bigbluebutton.BigBlueButtonMeetingTemplate;
+import org.olat.modules.bigbluebutton.BigBlueButtonServer;
 import org.olat.repository.RepositoryEntry;
 
 /**
@@ -110,6 +111,10 @@ public class BigBlueButtonMeetingImpl implements Persistable, BigBlueButtonMeeti
 	@ManyToOne(targetEntity=BigBlueButtonMeetingTemplateImpl.class, fetch=FetchType.LAZY, optional=true)
 	@JoinColumn(name="fk_template_id", nullable=true, insertable=true, updatable=true)
 	private BigBlueButtonMeetingTemplate template;
+	
+	@ManyToOne(targetEntity=BigBlueButtonServerImpl.class, fetch=FetchType.LAZY, optional=true)
+	@JoinColumn(name="fk_server_id", nullable=true, insertable=true, updatable=true)
+	private BigBlueButtonServer server;
 
 	@Override
 	public Long getKey() {
@@ -301,6 +306,15 @@ public class BigBlueButtonMeetingImpl implements Persistable, BigBlueButtonMeeti
 		this.template = template;
 	}
 
+	@Override
+	public BigBlueButtonServer getServer() {
+		return server;
+	}
+
+	public void setServer(BigBlueButtonServer server) {
+		this.server = server;
+	}
+
 	@Override
 	public int hashCode() {
 		return getKey() == null ? 964210765 : getKey().hashCode();
diff --git a/src/main/java/org/olat/modules/bigbluebutton/model/BigBlueButtonMeetingInfos.java b/src/main/java/org/olat/modules/bigbluebutton/model/BigBlueButtonMeetingInfos.java
new file mode 100644
index 0000000000000000000000000000000000000000..5f0fea5e1faa625f3d7de792f4c6da37ee3eb3ea
--- /dev/null
+++ b/src/main/java/org/olat/modules/bigbluebutton/model/BigBlueButtonMeetingInfos.java
@@ -0,0 +1,103 @@
+/**
+ * <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.modules.bigbluebutton.model;
+
+/**
+ * 
+ * Initial date: 8 avr. 2020<br>
+ * @author srosse, stephane.rosse@frentix.com, http://www.frentix.com
+ *
+ */
+public class BigBlueButtonMeetingInfos {
+	
+	private final String meetingId;
+	
+	private long videoCount;
+	private long listenerCount;
+	private long voiceParticipantCount;
+
+	private long participantCount;
+	private long moderatorCount;
+	
+	public BigBlueButtonMeetingInfos(String meetingId) {
+		this.meetingId = meetingId;
+	}
+	
+	public String getMeetingId() {
+		return meetingId;
+	}
+
+	public long getVideoCount() {
+		return videoCount;
+	}
+
+	public void setVideoCount(long videoCount) {
+		this.videoCount = videoCount;
+	}
+
+	public long getListenerCount() {
+		return listenerCount;
+	}
+
+	public void setListenerCount(long listenerCount) {
+		this.listenerCount = listenerCount;
+	}
+
+	public long getVoiceParticipantCount() {
+		return voiceParticipantCount;
+	}
+
+	public void setVoiceParticipantCount(long voiceParticipantCount) {
+		this.voiceParticipantCount = voiceParticipantCount;
+	}
+
+	public long getParticipantCount() {
+		return participantCount;
+	}
+
+	public void setParticipantCount(long participantCount) {
+		this.participantCount = participantCount;
+	}
+
+	public long getModeratorCount() {
+		return moderatorCount;
+	}
+
+	public void setModeratorCount(long moderatorCount) {
+		this.moderatorCount = moderatorCount;
+	}
+
+	@Override
+	public int hashCode() {
+		return meetingId == null ? 127846 : meetingId.hashCode();
+	}
+
+	@Override
+	public boolean equals(Object obj) {
+		if(this == obj) {
+			return true;
+		}
+		if(obj instanceof BigBlueButtonMeetingInfos) {
+			BigBlueButtonMeetingInfos meeting = (BigBlueButtonMeetingInfos)obj;
+			return meetingId != null && meetingId.equals(meeting.getMeetingId());
+		}
+		return false;
+	}
+}
diff --git a/src/main/java/org/olat/modules/bigbluebutton/model/BigBlueButtonServerImpl.java b/src/main/java/org/olat/modules/bigbluebutton/model/BigBlueButtonServerImpl.java
new file mode 100644
index 0000000000000000000000000000000000000000..b3911110930ac34b3c9aff2c0fc5746098be2fe8
--- /dev/null
+++ b/src/main/java/org/olat/modules/bigbluebutton/model/BigBlueButtonServerImpl.java
@@ -0,0 +1,184 @@
+/**
+ * <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.modules.bigbluebutton.model;
+
+import java.util.Date;
+
+import javax.persistence.Column;
+import javax.persistence.Entity;
+import javax.persistence.GeneratedValue;
+import javax.persistence.GenerationType;
+import javax.persistence.Id;
+import javax.persistence.Table;
+import javax.persistence.Temporal;
+import javax.persistence.TemporalType;
+
+import org.olat.core.id.Persistable;
+import org.olat.modules.bigbluebutton.BigBlueButtonServer;
+
+/**
+ * 
+ * Initial date: 7 avr. 2020<br>
+ * @author srosse, stephane.rosse@frentix.com, http://www.frentix.com
+ *
+ */
+@Entity(name="bigbluebuttonserver")
+@Table(name="o_bbb_server")
+public class BigBlueButtonServerImpl implements Persistable, BigBlueButtonServer {
+
+	private static final long serialVersionUID = 8664921045147695070L;
+
+	@Id
+	@GeneratedValue(strategy = GenerationType.IDENTITY)
+	@Column(name="id", nullable=false, unique=true, insertable=true, updatable=false)
+	private Long key;
+	
+	@Temporal(TemporalType.TIMESTAMP)
+	@Column(name="creationdate", nullable=false, insertable=true, updatable=false)
+	private Date creationDate;
+	@Temporal(TemporalType.TIMESTAMP)
+	@Column(name="lastmodified", nullable=false, insertable=true, updatable=true)
+	private Date lastModified;
+	
+	@Column(name="b_name", nullable=true, insertable=true, updatable=true)
+	private String name;
+	
+	@Column(name="b_url", nullable=false, insertable=true, updatable=true)
+	private String url;
+	@Column(name="b_shared_secret", nullable=false, insertable=true, updatable=true)
+	private String sharedSecret;
+	@Column(name="b_recording_url", nullable=true, insertable=true, updatable=true)
+	private String recordingUrl;
+
+	@Column(name="b_capacity_factor", nullable=false, insertable=true, updatable=true)
+	private Double capacityFactory;
+	@Column(name="b_enabled", nullable=false, insertable=true, updatable=true)
+	private boolean enabled;
+
+	@Override
+	public Long getKey() {
+		return key;
+	}
+
+	public void setKey(Long key) {
+		this.key = key;
+	}
+
+	@Override
+	public Date getCreationDate() {
+		return creationDate;
+	}
+
+	public void setCreationDate(Date creationDate) {
+		this.creationDate = creationDate;
+	}
+
+	@Override
+	public Date getLastModified() {
+		return lastModified;
+	}
+
+	@Override
+	public void setLastModified(Date lastModified) {
+		this.lastModified = lastModified;
+	}
+
+	@Override
+	public String getName() {
+		return name;
+	}
+	
+	@Override
+	public void setName(String name) {
+		this.name = name;
+	}
+
+	@Override
+	public String getUrl() {
+		return url;
+	}
+
+	@Override
+	public void setUrl(String url) {
+		this.url = url;
+	}
+
+	@Override
+	public String getSharedSecret() {
+		return sharedSecret;
+	}
+
+	@Override
+	public void setSharedSecret(String secret) {
+		this.sharedSecret = secret;
+	}
+
+	@Override
+	public String getRecordingUrl() {
+		return recordingUrl;
+	}
+
+	@Override
+	public void setRecordingUrl(String recordingUrl) {
+		this.recordingUrl = recordingUrl;
+	}
+
+	@Override
+	public boolean isEnabled() {
+		return enabled;
+	}
+
+	@Override
+	public void setEnabled(boolean enabled) {
+		this.enabled = enabled;
+	}
+
+	@Override
+	public Double getCapacityFactory() {
+		return capacityFactory;
+	}
+
+	@Override
+	public void setCapacityFactory(Double capacityFactory) {
+		this.capacityFactory = capacityFactory;
+	}
+
+	@Override
+	public int hashCode() {
+		return getKey() == null ? -378178 : getKey().hashCode();
+	}
+
+	@Override
+	public boolean equals(Object obj) {
+		if(obj == this) {
+			return true;
+		}
+		if(obj instanceof BigBlueButtonServerImpl) {
+			BigBlueButtonServerImpl server = (BigBlueButtonServerImpl)obj;
+			return getKey() != null && getKey().equals(server.getKey());
+		}
+		return false;
+	}
+
+	@Override
+	public boolean equalsByPersistableKey(Persistable persistable) {
+		return equals(persistable);
+	}
+}
diff --git a/src/main/java/org/olat/modules/bigbluebutton/ui/BigBlueButtonAdminController.java b/src/main/java/org/olat/modules/bigbluebutton/ui/BigBlueButtonAdminController.java
index 0d1b224c6f876db76a2da4842c515318f099ef15..c3dd16119d4ca09bab025cbb02e11a765ff00e2f 100644
--- a/src/main/java/org/olat/modules/bigbluebutton/ui/BigBlueButtonAdminController.java
+++ b/src/main/java/org/olat/modules/bigbluebutton/ui/BigBlueButtonAdminController.java
@@ -47,6 +47,7 @@ import org.olat.core.util.resource.OresHelper;
 public class BigBlueButtonAdminController extends BasicController implements Activateable2 {
 	
 	private Link configurationLink;
+	//private final Link serversLink;
 	private final Link meetingsLink;
 	private final Link templatesLink;
 	private final Link calendarLink;
@@ -68,11 +69,13 @@ public class BigBlueButtonAdminController extends BasicController implements Act
 		
 		mainVC = createVelocityContainer("bbb_admin");
 		
-			segmentView = SegmentViewFactory.createSegmentView("segments", mainVC, this);
+		segmentView = SegmentViewFactory.createSegmentView("segments", mainVC, this);
 		if(!configurationReadOnly) {
 			configurationLink = LinkFactory.createLink("account.configuration", mainVC, this);
 			segmentView.addSegment(configurationLink, true);
 		}
+		//serversLink = LinkFactory.createLink("servers.title", mainVC, this);
+		//segmentView.addSegment(serversLink, false);
 		templatesLink = LinkFactory.createLink("templates.title", mainVC, this);
 		segmentView.addSegment(templatesLink, false);
 		meetingsLink = LinkFactory.createLink("meetings.title", mainVC, this);
diff --git a/src/main/java/org/olat/modules/bigbluebutton/ui/BigBlueButtonAdminServersController.java b/src/main/java/org/olat/modules/bigbluebutton/ui/BigBlueButtonAdminServersController.java
new file mode 100644
index 0000000000000000000000000000000000000000..b9d9fa5ccf5c792cd4a109b6e8490bf555a69c3b
--- /dev/null
+++ b/src/main/java/org/olat/modules/bigbluebutton/ui/BigBlueButtonAdminServersController.java
@@ -0,0 +1,56 @@
+/**
+ * <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.modules.bigbluebutton.ui;
+
+import org.olat.core.gui.UserRequest;
+import org.olat.core.gui.components.form.flexible.FormItemContainer;
+import org.olat.core.gui.components.form.flexible.impl.FormBasicController;
+import org.olat.core.gui.control.Controller;
+import org.olat.core.gui.control.WindowControl;
+
+/**
+ * 
+ * Initial date: 7 avr. 2020<br>
+ * @author srosse, stephane.rosse@frentix.com, http://www.frentix.com
+ *
+ */
+public class BigBlueButtonAdminServersController extends FormBasicController {
+	
+	public BigBlueButtonAdminServersController(UserRequest ureq, WindowControl wControl) {
+		super(ureq, wControl);
+		
+		initForm(ureq);
+	}
+
+	@Override
+	protected void initForm(FormItemContainer formLayout, Controller listener, UserRequest ureq) {
+		//
+	}
+
+	@Override
+	protected void doDispose() {
+		//
+	}
+
+	@Override
+	protected void formOK(UserRequest ureq) {
+		//
+	}
+}
diff --git a/src/main/java/org/olat/modules/bigbluebutton/ui/BigBlueButtonAdminServersTableModel.java b/src/main/java/org/olat/modules/bigbluebutton/ui/BigBlueButtonAdminServersTableModel.java
new file mode 100644
index 0000000000000000000000000000000000000000..39c4538ad4e04a36b1a21779216931badbb32fe7
--- /dev/null
+++ b/src/main/java/org/olat/modules/bigbluebutton/ui/BigBlueButtonAdminServersTableModel.java
@@ -0,0 +1,69 @@
+/**
+ * <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.modules.bigbluebutton.ui;
+
+import java.util.Locale;
+
+import org.olat.core.commons.persistence.SortKey;
+import org.olat.core.gui.components.form.flexible.impl.elements.table.DefaultFlexiTableDataModel;
+import org.olat.core.gui.components.form.flexible.impl.elements.table.FlexiTableColumnModel;
+import org.olat.core.gui.components.form.flexible.impl.elements.table.SortableFlexiTableDataModel;
+import org.olat.modules.bigbluebutton.BigBlueButtonServer;
+
+/**
+ * 
+ * Initial date: 7 avr. 2020<br>
+ * @author srosse, stephane.rosse@frentix.com, http://www.frentix.com
+ *
+ */
+public class BigBlueButtonAdminServersTableModel extends DefaultFlexiTableDataModel<BigBlueButtonServer>
+implements SortableFlexiTableDataModel<BigBlueButtonServer> {
+
+	private final Locale locale;
+	
+	public BigBlueButtonAdminServersTableModel(FlexiTableColumnModel columnsModel, Locale locale) {
+		super(columnsModel);
+		this.locale = locale;
+	}
+
+	@Override
+	public void sort(SortKey sortKey) {
+		//
+	}
+
+	@Override
+	public Object getValueAt(int row, int col) {
+		// TODO Auto-generated method stub
+		return null;
+	}
+
+	@Override
+	public Object getValueAt(BigBlueButtonServer row, int col) {
+		return null;
+	}
+
+	@Override
+	public DefaultFlexiTableDataModel<BigBlueButtonServer> createCopyWithEmptyList() {
+		return null;
+	}
+	
+	
+
+}
diff --git a/src/main/java/org/olat/modules/bigbluebutton/ui/BigBlueButtonConfigurationController.java b/src/main/java/org/olat/modules/bigbluebutton/ui/BigBlueButtonConfigurationController.java
index 24b9f3a7c8fc62a0e51888c9f532bd5dcb014356..23e972292492f35242e2b4c63bfad4cadc4a7fcb 100644
--- a/src/main/java/org/olat/modules/bigbluebutton/ui/BigBlueButtonConfigurationController.java
+++ b/src/main/java/org/olat/modules/bigbluebutton/ui/BigBlueButtonConfigurationController.java
@@ -19,28 +19,31 @@
  */
 package org.olat.modules.bigbluebutton.ui;
 
-import java.net.URI;
-import java.net.URISyntaxException;
+import java.util.List;
 
 import org.olat.collaboration.CollaborationToolsFactory;
 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.FlexiTableElement;
 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.elements.SpacerElement;
-import org.olat.core.gui.components.form.flexible.elements.TextElement;
 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.form.flexible.impl.elements.table.DefaultFlexiColumnModel;
+import org.olat.core.gui.components.form.flexible.impl.elements.table.FlexiTableColumnModel;
+import org.olat.core.gui.components.form.flexible.impl.elements.table.FlexiTableDataModelFactory;
+import org.olat.core.gui.components.form.flexible.impl.elements.table.SelectionEvent;
 import org.olat.core.gui.components.link.Link;
 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.util.StringHelper;
+import org.olat.core.gui.control.generic.closablewrapper.CloseableModalController;
 import org.olat.modules.bigbluebutton.BigBlueButtonManager;
 import org.olat.modules.bigbluebutton.BigBlueButtonModule;
-import org.olat.modules.bigbluebutton.model.BigBlueButtonErrors;
-import org.olat.modules.bigbluebutton.model.BigBlueButtonException;
+import org.olat.modules.bigbluebutton.BigBlueButtonServer;
+import org.olat.modules.bigbluebutton.ui.BigBlueButtonConfigurationServersTableModel.ConfigServerCols;
 import org.springframework.beans.factory.annotation.Autowired;
 
 /**
@@ -52,21 +55,19 @@ import org.springframework.beans.factory.annotation.Autowired;
 public class BigBlueButtonConfigurationController extends FormBasicController {
 
 	private static final String[] FOR_KEYS = { "courses", "groups" };
-	private static final String PLACEHOLDER = "xxx-placeholder-xxx";
+	private static final String[] ENABLED_KEY = new String[]{ "on" };
 	
-	private FormLink checkLink;
-	private TextElement urlEl;
-	private SpacerElement spacerEl;
-	private TextElement sharedSecretEl;
 	private MultipleSelectionElement moduleEnabled;
 	private MultipleSelectionElement enabledForEl;
 	private MultipleSelectionElement permanentForEl;
-	private MultipleSelectionElement adhocForEl;
-
-	private static final String[] enabledKeys = new String[]{"on"};
-	private final String[] enabledValues;
 	
-	private String replacedSharedSecretValue;
+	private FormLink addServerButton;
+	private FlexiTableElement serversTableEl;
+	private BigBlueButtonConfigurationServersTableModel serversTableModel;
+	
+	private CloseableModalController cmc;
+	private EditBigBlueButtonServerController editServerCtlr; 
+	private ConfirmDeleteServerController confirmDeleteServerCtrl;
 	
 	@Autowired
 	private BigBlueButtonModule bigBlueButtonModule;
@@ -75,9 +76,10 @@ public class BigBlueButtonConfigurationController extends FormBasicController {
 	
 	public BigBlueButtonConfigurationController(UserRequest ureq, WindowControl wControl) {
 		super(ureq, wControl);
-		enabledValues = new String[]{translate("enabled")};
+		
 		initForm(ureq);
 		updateUI();
+		loadModel();
 	}
 
 	@Override
@@ -85,9 +87,10 @@ public class BigBlueButtonConfigurationController extends FormBasicController {
 		setFormTitle("bigbluebutton.title");
 		setFormInfo("bigbluebutton.intro");
 		setFormContextHelp("Communication and Collaboration#_bigbluebutton_config");
+		String[] enabledValues = new String[]{ translate("enabled") };
 		
-		moduleEnabled = uifactory.addCheckboxesHorizontal("bigbluebutton.module.enabled", formLayout, enabledKeys, enabledValues);
-		moduleEnabled.select(enabledKeys[0], bigBlueButtonModule.isEnabled());
+		moduleEnabled = uifactory.addCheckboxesHorizontal("bigbluebutton.module.enabled", formLayout, ENABLED_KEY, enabledValues);
+		moduleEnabled.select(ENABLED_KEY[0], bigBlueButtonModule.isEnabled());
 		moduleEnabled.addActionListener(FormEvent.ONCHANGE);
 		
 		String[] forValues = new String[] {
@@ -97,38 +100,28 @@ public class BigBlueButtonConfigurationController extends FormBasicController {
 		enabledForEl.select(FOR_KEYS[0], bigBlueButtonModule.isCoursesEnabled());
 		enabledForEl.select(FOR_KEYS[1], bigBlueButtonModule.isGroupsEnabled());
 		
-		permanentForEl = uifactory.addCheckboxesHorizontal("enable.permanent.meeting", formLayout, enabledKeys, enabledValues);
-		permanentForEl.select(enabledKeys[0], bigBlueButtonModule.isPermanentMeetingEnabled());
-		
-		adhocForEl = uifactory.addCheckboxesHorizontal("enable.adhoc.meeting", formLayout, enabledKeys, enabledValues);
-		adhocForEl.select(enabledKeys[0], bigBlueButtonModule.isAdhocMeetingEnabled());
-		adhocForEl.setVisible(false);//TODO bbb
-		
-		//spacer
-		spacerEl = uifactory.addSpacerElement("spacer", formLayout, false);
+		permanentForEl = uifactory.addCheckboxesHorizontal("enable.permanent.meeting", formLayout, ENABLED_KEY, enabledValues);
+		permanentForEl.select(ENABLED_KEY[0], bigBlueButtonModule.isPermanentMeetingEnabled());
 
-		URI uri = bigBlueButtonModule.getBigBlueButtonURI();
-		String uriStr = uri == null ? "" : uri.toString();
-		urlEl = uifactory.addTextElement("bbb-url", "option.baseurl", 255, uriStr, formLayout);
-		urlEl.setDisplaySize(60);
-		urlEl.setExampleKey("option.baseurl.example", null);
-		urlEl.setMandatory(true);
-
-		String sharedSecret = bigBlueButtonModule.getSharedSecret();
-		if(StringHelper.containsNonWhitespace(sharedSecret)) {
-			replacedSharedSecretValue = sharedSecret;
-			sharedSecret = PLACEHOLDER;
-		}
-		sharedSecretEl = uifactory.addPasswordElement("shared.secret", "option.bigbluebutton.shared.secret", 255, sharedSecret, formLayout);
-		sharedSecretEl.setAutocomplete("new-password");
-		sharedSecretEl.setMandatory(true);
+		FlexiTableColumnModel columnsModel = FlexiTableDataModelFactory.createFlexiTableColumnModel();
+		columnsModel.addFlexiColumnModel(new DefaultFlexiColumnModel(ConfigServerCols.url));
+		columnsModel.addFlexiColumnModel(new DefaultFlexiColumnModel(ConfigServerCols.enabled));
+		columnsModel.addFlexiColumnModel(new DefaultFlexiColumnModel("edit", translate("edit"), "edit"));
+		columnsModel.addFlexiColumnModel(new DefaultFlexiColumnModel("delete", translate("delete"), "delete"));
+		serversTableModel = new BigBlueButtonConfigurationServersTableModel(columnsModel, getLocale());
 		
+		serversTableEl = uifactory.addTableElement(getWindowControl(), "servers", serversTableModel, 10, false, getTranslator(), formLayout);
+		serversTableEl.setCustomizeColumns(false);
+		serversTableEl.setNumOfRowsEnabled(false);
+		serversTableEl.setLabel("bigbluebutton.servers", null);
+		serversTableEl.setEmtpyTableMessageKey("bigbluebutton.servers.empty");
 		
+		addServerButton = uifactory.addFormLink("add.server", formLayout, Link.BUTTON);
+
 		//buttons save - check
 		FormLayoutContainer buttonLayout = FormLayoutContainer.createButtonLayout("save", getTranslator());
 		formLayout.add(buttonLayout);
 		uifactory.addFormSubmitButton("save", buttonLayout);
-		checkLink = uifactory.addFormLink("check", buttonLayout, Link.BUTTON);
 	}
 
 	@Override
@@ -138,13 +131,16 @@ public class BigBlueButtonConfigurationController extends FormBasicController {
 	
 	private void updateUI() {
 		boolean enabled = moduleEnabled.isAtLeastSelected(1);
-		adhocForEl.setVisible(false);
 		permanentForEl.setVisible(enabled);
 		enabledForEl.setVisible(enabled);
-		checkLink.setVisible(enabled);
-		urlEl.setVisible(enabled);
-		sharedSecretEl.setVisible(enabled);
-		spacerEl.setVisible(enabled);
+		serversTableEl.setVisible(enabled);
+		addServerButton.setVisible(enabled);
+	}
+	
+	private void loadModel() {
+		List<BigBlueButtonServer> servers = bigBlueButtonManager.getServers();
+		serversTableModel.setObjects(servers);
+		serversTableEl.reset(true, true, true);
 	}
 	
 	@Override
@@ -153,67 +149,52 @@ public class BigBlueButtonConfigurationController extends FormBasicController {
 		
 		//validate only if the module is enabled
 		if(moduleEnabled.isAtLeastSelected(1)) {
-			allOk &= validateUrlFields();
-			if(allOk) {
-				allOk &= validateConnection();
+			if(serversTableModel.getRowCount() == 0) {
+				serversTableEl.setErrorKey("form.legende.mandatory", null);
+				allOk &= false;
 			}
 		}
 		
 		return allOk;
 	}
 
-	private boolean validateUrlFields() {
-		boolean allOk = true;
-		
-		String url = urlEl.getValue();
-		urlEl.clearError();
-		if(StringHelper.containsNonWhitespace(url)) {
-			try {
-				URI uri = new URI(url);
-				uri.getHost();
-			} catch(Exception e) {
-				urlEl.setErrorKey("error.url.invalid", null);
-				allOk &= false;
+	@Override
+	protected void event(UserRequest ureq, Controller source, Event event) {
+		if(editServerCtlr == source || confirmDeleteServerCtrl == source) {
+			if(event == Event.CHANGED_EVENT || event == Event.DONE_EVENT) {
+				loadModel();
 			}
-		} else {
-			urlEl.setErrorKey("form.legende.mandatory", null);
-			allOk &= false;
+			cmc.deactivate();
+			cleanUp();
+		} else if(cmc == source) {
+			cleanUp();
 		}
-		
-		String password = sharedSecretEl.getValue();
-		sharedSecretEl.clearError();
-		if(!StringHelper.containsNonWhitespace(password)) {
-			sharedSecretEl.setErrorKey("form.legende.mandatory", null);
-			allOk &= false;
-		}
-		
-		return allOk;
+		super.event(ureq, source, event);
 	}
 	
-	private boolean validateConnection() {
-		boolean allOk = true;
-		try {
-			BigBlueButtonErrors errors = new BigBlueButtonErrors();
-			boolean ok = checkConnection(errors);
-			if(!ok || errors.hasErrors()) {
-				sharedSecretEl.setValue("");
-				urlEl.setErrorKey("error.connectionValidationFailed", new String[] {errors.getErrorMessages()});
-				allOk &= false;
-			}
-		} catch (Exception e) {
-			showError(BigBlueButtonException.SERVER_NOT_I18N_KEY);
-			allOk &= false;
-		}
-		return allOk;
+	private void cleanUp() {
+		removeAsListenerAndDispose(confirmDeleteServerCtrl);
+		removeAsListenerAndDispose(editServerCtlr);
+		removeAsListenerAndDispose(cmc);
+		confirmDeleteServerCtrl = null;
+		editServerCtlr = null;
+		cmc = null;
 	}
-	
+
 	@Override
 	protected void formInnerEvent(UserRequest ureq, FormItem source, FormEvent event) {
 		if(source == moduleEnabled) {
 			updateUI();
-		} else if(source == checkLink) {
-			if(validateUrlFields()) {
-				doCheckConnection();
+		} else if(addServerButton == source) {
+			addServer(ureq);
+		} else if(serversTableEl == source) {
+			if(event instanceof SelectionEvent) {
+				SelectionEvent se = (SelectionEvent)event;
+				if("edit".equals(se.getCommand())) {
+					doEditServer(ureq, serversTableModel.getObject(se.getIndex()));
+				} else if("delete".equals(se.getCommand())) {
+					doConfirmDelete(ureq, serversTableModel.getObject(se.getIndex()));
+				}
 			}
 		}
 		super.formInnerEvent(ureq, source, event);
@@ -221,63 +202,50 @@ public class BigBlueButtonConfigurationController extends FormBasicController {
 	
 	@Override
 	protected void formOK(UserRequest ureq) {
-		try {
-			boolean enabled = moduleEnabled.isSelected(0);
-			bigBlueButtonModule.setEnabled(enabled);
-			// update collaboration tools list
-			if(enabled) {
-				String url = urlEl.getValue();
-				bigBlueButtonModule.setBigBlueButtonURI(new URI(url));
-				bigBlueButtonModule.setCoursesEnabled(enabledForEl.isSelected(0));
-				bigBlueButtonModule.setGroupsEnabled(enabledForEl.isSelected(1));
-				bigBlueButtonModule.setPermanentMeetingEnabled(permanentForEl.isAtLeastSelected(1));
-				bigBlueButtonModule.setAdhocMeetingEnabled(adhocForEl.isAtLeastSelected(1));
-				
-				String sharedSecret = sharedSecretEl.getValue();
-				if(!PLACEHOLDER.equals(sharedSecret)) {
-					bigBlueButtonModule.setSharedSecret(sharedSecret);
-					sharedSecretEl.setValue(PLACEHOLDER);
-				} else if(StringHelper.containsNonWhitespace(replacedSharedSecretValue)) {
-					bigBlueButtonModule.setSharedSecret(replacedSharedSecretValue);
-				}
-			} else {
-				bigBlueButtonModule.setBigBlueButtonURI(null);
-				bigBlueButtonModule.setSecret(null);
-				bigBlueButtonModule.setSharedSecret(null);
-			}
-			CollaborationToolsFactory.getInstance().initAvailableTools();
-		} catch (URISyntaxException e) {
-			logError("", e);
-			urlEl.setErrorKey("error.url.invalid", null);
+		boolean enabled = moduleEnabled.isSelected(0);
+		bigBlueButtonModule.setEnabled(enabled);
+		// update collaboration tools list
+		if(enabled) {
+			bigBlueButtonModule.setCoursesEnabled(enabledForEl.isSelected(0));
+			bigBlueButtonModule.setGroupsEnabled(enabledForEl.isSelected(1));
+			bigBlueButtonModule.setPermanentMeetingEnabled(permanentForEl.isAtLeastSelected(1));
 		}
+		CollaborationToolsFactory.getInstance().initAvailableTools();
 	}
 	
-	private void doCheckConnection() {
-		BigBlueButtonErrors errors = new BigBlueButtonErrors();
-		boolean loginOk = checkConnection(errors);
-		if(errors.hasErrors()) {
-			getWindowControl().setError(BigBlueButtonErrorHelper.formatErrors(getTranslator(), errors));
-		} else if(loginOk) {
-			showInfo("connection.successful");
-		} else {
-			showError("connection.failed");
-		}
+	private void addServer(UserRequest ureq) {
+		if(guardModalController(editServerCtlr)) return;
+
+		editServerCtlr = new EditBigBlueButtonServerController(ureq, getWindowControl());
+		listenTo(editServerCtlr);
+		
+		cmc = new CloseableModalController(getWindowControl(), "close", editServerCtlr.getInitialComponent(),
+				true, translate("add.single.meeting"));
+		cmc.activate();
+		listenTo(cmc);
 	}
 	
-	private boolean checkConnection(BigBlueButtonErrors errors) {
-		String url = urlEl.getValue();
-		String sharedSecret = sharedSecretEl.getValue();
-		if(PLACEHOLDER.equals(sharedSecret)) {
-			if(StringHelper.containsNonWhitespace(replacedSharedSecretValue)) {
-				sharedSecret = replacedSharedSecretValue;
-			} else {
-				sharedSecret = bigBlueButtonModule.getSharedSecret();
-			}
-		} else {
-			replacedSharedSecretValue = sharedSecret;
-			sharedSecretEl.setValue(PLACEHOLDER);
-		}
+	private void doEditServer(UserRequest ureq, BigBlueButtonServer server) {
+		if(guardModalController(editServerCtlr)) return;
+
+		editServerCtlr = new EditBigBlueButtonServerController(ureq, getWindowControl(), server);
+		listenTo(editServerCtlr);
+		
+		String title = translate("edit.server", new String[] { server.getUrl() });
+		cmc = new CloseableModalController(getWindowControl(), "close", editServerCtlr.getInitialComponent(), true, title);
+		cmc.activate();
+		listenTo(cmc);
+	}
+	
+	private void doConfirmDelete(UserRequest ureq, BigBlueButtonServer server) {
+		if(guardModalController(confirmDeleteServerCtrl)) return;
+
+		confirmDeleteServerCtrl = new ConfirmDeleteServerController(ureq, getWindowControl(), server);
+		listenTo(confirmDeleteServerCtrl);
 		
-		return bigBlueButtonManager.checkConnection(url, sharedSecret, errors);
+		String title = translate("confirm.delete.server.title", new String[] { server.getUrl() });
+		cmc = new CloseableModalController(getWindowControl(), "close", confirmDeleteServerCtrl.getInitialComponent(), true, title);
+		cmc.activate();
+		listenTo(cmc);
 	}
 }
diff --git a/src/main/java/org/olat/modules/bigbluebutton/ui/BigBlueButtonConfigurationServersTableModel.java b/src/main/java/org/olat/modules/bigbluebutton/ui/BigBlueButtonConfigurationServersTableModel.java
new file mode 100644
index 0000000000000000000000000000000000000000..305903dc29176c733a5f59d21be96a1a2626c6c5
--- /dev/null
+++ b/src/main/java/org/olat/modules/bigbluebutton/ui/BigBlueButtonConfigurationServersTableModel.java
@@ -0,0 +1,102 @@
+/**
+ * <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.modules.bigbluebutton.ui;
+
+import java.util.Locale;
+
+import org.olat.core.commons.persistence.SortKey;
+import org.olat.core.gui.components.form.flexible.impl.elements.table.DefaultFlexiTableDataModel;
+import org.olat.core.gui.components.form.flexible.impl.elements.table.FlexiSortableColumnDef;
+import org.olat.core.gui.components.form.flexible.impl.elements.table.FlexiTableColumnModel;
+import org.olat.core.gui.components.form.flexible.impl.elements.table.SortableFlexiTableDataModel;
+import org.olat.modules.bigbluebutton.BigBlueButtonServer;
+
+/**
+ * 
+ * Initial date: 7 avr. 2020<br>
+ * @author srosse, stephane.rosse@frentix.com, http://www.frentix.com
+ *
+ */
+public class BigBlueButtonConfigurationServersTableModel extends DefaultFlexiTableDataModel<BigBlueButtonServer>
+implements SortableFlexiTableDataModel<BigBlueButtonServer> {
+	
+	private final static ConfigServerCols[] COLS = ConfigServerCols.values();
+
+	private final Locale locale;
+	
+	public BigBlueButtonConfigurationServersTableModel(FlexiTableColumnModel columnsModel, Locale locale) {
+		super(columnsModel);
+		this.locale = locale;
+	}
+
+	@Override
+	public void sort(SortKey sortKey) {
+		//
+	}
+
+	@Override
+	public Object getValueAt(int row, int col) {
+		BigBlueButtonServer server = getObject(row);
+		return getValueAt(server, col);
+	}
+
+	@Override
+	public Object getValueAt(BigBlueButtonServer row, int col) {
+		switch(COLS[col]) {
+			case url: return row.getUrl();
+			case recordingUrl: return row.getRecordingUrl();
+			case enabled: return Boolean.valueOf(row.isEnabled());
+			default: return "ERROR";
+		}
+	}
+
+	@Override
+	public BigBlueButtonConfigurationServersTableModel createCopyWithEmptyList() {
+		return new BigBlueButtonConfigurationServersTableModel(getTableColumnModel(), locale);
+	}
+	
+	public enum ConfigServerCols implements FlexiSortableColumnDef {
+		
+		url("table.header.server.url"),
+		recordingUrl("table.header.server.recording"),
+		enabled("table.header.server.enabled");
+		
+		private final String i18nHeaderKey;
+		
+		private ConfigServerCols(String i18nHeaderKey) {
+			this.i18nHeaderKey = i18nHeaderKey;
+		}
+
+		@Override
+		public boolean sortable() {
+			return true;
+		}
+
+		@Override
+		public String sortKey() {
+			return name();
+		}
+
+		@Override
+		public String i18nHeaderKey() {
+			return i18nHeaderKey;
+		}
+	}
+}
diff --git a/src/main/java/org/olat/modules/bigbluebutton/ui/ConfirmDeleteServerController.java b/src/main/java/org/olat/modules/bigbluebutton/ui/ConfirmDeleteServerController.java
new file mode 100644
index 0000000000000000000000000000000000000000..ecc273d972d48053a8d485bea07be0528c98e55c
--- /dev/null
+++ b/src/main/java/org/olat/modules/bigbluebutton/ui/ConfirmDeleteServerController.java
@@ -0,0 +1,104 @@
+/**
+ * <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.modules.bigbluebutton.ui;
+
+import org.olat.core.commons.persistence.DB;
+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.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.control.Controller;
+import org.olat.core.gui.control.Event;
+import org.olat.core.gui.control.WindowControl;
+import org.olat.modules.bigbluebutton.BigBlueButtonManager;
+import org.olat.modules.bigbluebutton.BigBlueButtonServer;
+import org.olat.modules.bigbluebutton.model.BigBlueButtonErrors;
+import org.springframework.beans.factory.annotation.Autowired;
+
+/**
+ * 
+ * Initial date: 8 avr. 2020<br>
+ * @author srosse, stephane.rosse@frentix.com, http://www.frentix.com
+ *
+ */
+public class ConfirmDeleteServerController extends FormBasicController {
+	
+	private FormLink deleteLink;
+	
+	private BigBlueButtonServer server;
+	
+	@Autowired
+	private DB dbInstance;
+	@Autowired
+	private BigBlueButtonManager bigBlueButtonManager;
+	
+	public ConfirmDeleteServerController(UserRequest ureq, WindowControl wControl, BigBlueButtonServer server) {
+		super(ureq, wControl, "confirm_delete_server");
+		this.server = server;
+		initForm(ureq);
+	}
+
+	@Override
+	protected void initForm(FormItemContainer formLayout, Controller listener, UserRequest ureq) {
+		if(formLayout instanceof FormLayoutContainer) {
+			FormLayoutContainer layoutCont = (FormLayoutContainer)formLayout;
+			String msg = translate("confirm.delete.server", new String[] { server.getUrl() });
+			layoutCont.contextPut("msg", msg);
+		}
+
+		uifactory.addFormCancelButton("cancel", formLayout, ureq, getWindowControl());
+		deleteLink = uifactory.addFormLink("delete", formLayout, Link.BUTTON);
+	}
+
+	@Override
+	protected void doDispose() {
+		//
+	}
+
+	@Override
+	protected void formInnerEvent(UserRequest ureq, FormItem source, FormEvent event) {
+		if(deleteLink == source) {
+			doDelete(ureq);
+		}
+		super.formInnerEvent(ureq, source, event);
+	}
+
+	@Override
+	protected void formOK(UserRequest ureq) {
+		//
+	}
+	
+	@Override
+	protected void formCancelled(UserRequest ureq) {
+		fireEvent(ureq, Event.CANCELLED_EVENT);
+	}
+
+	private void doDelete(UserRequest ureq) {
+		BigBlueButtonErrors errors = new BigBlueButtonErrors();
+		bigBlueButtonManager.deleteServer(server, errors);
+		dbInstance.commit();
+		fireEvent(ureq, Event.DONE_EVENT);
+	}
+
+}
diff --git a/src/main/java/org/olat/modules/bigbluebutton/ui/EditBigBlueButtonServerController.java b/src/main/java/org/olat/modules/bigbluebutton/ui/EditBigBlueButtonServerController.java
new file mode 100644
index 0000000000000000000000000000000000000000..a0acc91b71394b05ec455211b031c308fa299c79
--- /dev/null
+++ b/src/main/java/org/olat/modules/bigbluebutton/ui/EditBigBlueButtonServerController.java
@@ -0,0 +1,300 @@
+/**
+ * <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.modules.bigbluebutton.ui;
+
+import java.net.URI;
+
+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.elements.TextElement;
+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.control.Controller;
+import org.olat.core.gui.control.Event;
+import org.olat.core.gui.control.WindowControl;
+import org.olat.core.util.StringHelper;
+import org.olat.modules.bigbluebutton.BigBlueButtonManager;
+import org.olat.modules.bigbluebutton.BigBlueButtonServer;
+import org.olat.modules.bigbluebutton.model.BigBlueButtonErrors;
+import org.olat.modules.bigbluebutton.model.BigBlueButtonException;
+import org.springframework.beans.factory.annotation.Autowired;
+
+/**
+ * 
+ * Initial date: 7 avr. 2020<br>
+ * @author srosse, stephane.rosse@frentix.com, http://www.frentix.com
+ *
+ */
+public class EditBigBlueButtonServerController extends FormBasicController {
+
+	private static final String PLACEHOLDER = "xxx-placeholder-xxx";
+	private static final String[] onKeys = new String[] { "" };
+
+	private FormLink checkLink;
+	private TextElement urlEl;
+	//private TextElement recordingUrlEl;
+	private TextElement sharedSecretEl;
+	private TextElement capacityFactorEl;
+	private MultipleSelectionElement enabledEl;
+
+	private BigBlueButtonServer server;
+	private String replacedSharedSecretValue;
+
+	@Autowired
+	private BigBlueButtonManager bigBlueButtonManager;
+	
+	public EditBigBlueButtonServerController(UserRequest ureq, WindowControl wControl) {
+		super(ureq, wControl);
+		initForm(ureq);
+	}
+	
+	public EditBigBlueButtonServerController(UserRequest ureq, WindowControl wControl, BigBlueButtonServer server) {
+		super(ureq, wControl);
+		this.server = server;
+		initForm(ureq);
+	}
+
+	@Override
+	protected void initForm(FormItemContainer formLayout, Controller listener, UserRequest ureq) {
+		String url = server == null ? null : server.getUrl();
+		urlEl = uifactory.addTextElement("bbb.url", "option.baseurl", 255, url, formLayout);
+		urlEl.setDisplaySize(60);
+		urlEl.setExampleKey("option.baseurl.example", null);
+		urlEl.setMandatory(true);
+
+		String sharedSecret = server == null ? null : server.getSharedSecret();
+		if(StringHelper.containsNonWhitespace(sharedSecret)) {
+			replacedSharedSecretValue = sharedSecret;
+			sharedSecret = PLACEHOLDER;
+		}
+		sharedSecretEl = uifactory.addPasswordElement("shared.secret", "option.bigbluebutton.shared.secret", 255, sharedSecret, formLayout);
+		sharedSecretEl.setAutocomplete("new-password");
+		sharedSecretEl.setMandatory(true);
+		
+		//String recordingUrl = server == null ? null : server.getRecordingUrl();
+		//recordingUrlEl = uifactory.addTextElement("bbb.recording.url", "option.recordingurl", 255, recordingUrl, formLayout);
+		//recordingUrlEl.setDisplaySize(60);
+		//recordingUrlEl.setExampleKey("option.baseurl.example", null);
+		
+		String capacityFactor = server == null || server.getCapacityFactory() == null
+				? "1.0" : server.getCapacityFactory().toString();
+		capacityFactorEl = uifactory.addTextElement("bbb.capacity", "option.capacity.factory", 255, capacityFactor, formLayout);
+		capacityFactorEl.setDisplaySize(60);
+		capacityFactorEl.setExampleKey("option.capacity.factor.example", null);
+		
+		String[] onValues = new String[] { translate("enabled") };
+		enabledEl = uifactory.addCheckboxesVertical("option.enabled.server", formLayout, onKeys, onValues, 1);
+		enabledEl.select(onKeys[0], server == null || server.isEnabled());
+		
+		FormLayoutContainer buttonLayout = FormLayoutContainer.createButtonLayout("buttons", getTranslator());
+		formLayout.add("buttons", buttonLayout);
+		uifactory.addFormCancelButton("cancel", buttonLayout, ureq, getWindowControl());
+		uifactory.addFormSubmitButton("save", buttonLayout);
+		checkLink = uifactory.addFormLink("check", buttonLayout, Link.BUTTON);
+	}
+
+	@Override
+	protected void doDispose() {
+		//
+	}
+	
+	@Override
+	protected boolean validateFormLogic(UserRequest ureq) {
+		boolean allOk = super.validateFormLogic(ureq);
+
+		allOk &= validateUrlFields();
+		if(allOk) {
+			allOk &= validateConnection();
+			
+			if((server == null || server.getKey() == null)
+					&& bigBlueButtonManager.hasServer(urlEl.getValue())) {
+				urlEl.setErrorKey("error.server.exists", null);
+				allOk &= false;
+			}
+		}
+		
+		return allOk;
+	}
+	
+	private boolean validateUrlFields() {
+		boolean allOk = true;
+		
+		allOk &= validateUrl(urlEl, true);
+		//allOk &= validateUrl(recordingUrlEl, false);
+		
+		capacityFactorEl.clearError();
+		if(StringHelper.containsNonWhitespace(capacityFactorEl.getValue())) {
+			try {
+				String factor = capacityFactorEl.getValue();
+				double capacityFactory = Double.parseDouble(factor);
+				if(capacityFactory < 1.0 || capacityFactory > 100.0) {
+					capacityFactorEl.setErrorKey("error.capacity.factory", null);
+					allOk &= false;
+				}
+			} catch (NumberFormatException e) {
+				capacityFactorEl.setErrorKey("error.capacity.factory", null);
+				allOk &= false;
+			}
+		} else {
+			capacityFactorEl.setErrorKey("form.legende.mandatory", null);
+			allOk &= false;
+		}
+		
+		String password = sharedSecretEl.getValue();
+		sharedSecretEl.clearError();
+		if(!StringHelper.containsNonWhitespace(password)) {
+			sharedSecretEl.setErrorKey("form.legende.mandatory", null);
+			allOk &= false;
+		}
+		
+		return allOk;
+	}
+	
+	private boolean validateUrl(TextElement el, boolean mandatory) {
+		boolean allOk = true;
+		
+		String url = el.getValue();
+		el.clearError();
+		if(StringHelper.containsNonWhitespace(url)) {
+			try {
+				URI uri = new URI(url);
+				uri.getHost();
+			} catch(Exception e) {
+				el.setErrorKey("error.url.invalid", null);
+				allOk &= false;
+			}
+		} else if(mandatory) {
+			el.setErrorKey("form.legende.mandatory", null);
+			allOk &= false;
+		}
+		
+		return allOk;
+	}
+	
+	private boolean validateConnection() {
+		boolean allOk = true;
+		try {
+			BigBlueButtonErrors errors = new BigBlueButtonErrors();
+			boolean ok = checkConnection(errors);
+			if(!ok || errors.hasErrors()) {
+				sharedSecretEl.setValue("");
+				urlEl.setErrorKey("error.connectionValidationFailed", new String[] {errors.getErrorMessages()});
+				allOk &= false;
+			}
+		} catch (Exception e) {
+			showError(BigBlueButtonException.SERVER_NOT_I18N_KEY);
+			allOk &= false;
+		}
+		return allOk;
+	}
+	
+	@Override
+	protected void formInnerEvent(UserRequest ureq, FormItem source, FormEvent event) {
+		if(source == checkLink) {
+			if(validateUrlFields()) {
+				doCheckConnection();
+			}
+		}
+		super.formInnerEvent(ureq, source, event);
+	}
+	
+	private Double getCapacityFactory() {
+		String val = capacityFactorEl.getValue();
+		Double factor = null;
+		if(StringHelper.containsNonWhitespace(val)) {
+			try {
+				factor = Double.valueOf(val);
+			} catch (NumberFormatException e) {
+				logWarn("Cannot parse: " + val, null);
+			}
+		}
+		
+		if(factor == null || factor.doubleValue() < 1.0d) {
+			factor = Double.valueOf(1.0d);
+		}
+		return factor;
+	}
+
+	@Override
+	protected void formOK(UserRequest ureq) {
+		if(server == null) {
+			String url = urlEl.getValue();	
+			String sharedSecret = sharedSecretEl.getValue();
+			if(!PLACEHOLDER.equals(sharedSecret)) {
+				sharedSecretEl.setValue(PLACEHOLDER);
+			} else if(StringHelper.containsNonWhitespace(replacedSharedSecretValue)) {
+				sharedSecret = replacedSharedSecretValue;
+			}
+			server = bigBlueButtonManager.createServer(url, null, sharedSecret);
+		} else {
+			server.setUrl(urlEl.getValue());
+			String sharedSecret = sharedSecretEl.getValue();
+			if(!PLACEHOLDER.equals(sharedSecret)) {
+				server.setSharedSecret(sharedSecret);
+				sharedSecretEl.setValue(PLACEHOLDER);
+			} else if(StringHelper.containsNonWhitespace(replacedSharedSecretValue)) {
+				server.setSharedSecret(replacedSharedSecretValue);
+			}
+		}
+		
+		server.setEnabled(enabledEl.isAtLeastSelected(1));
+		server.setCapacityFactory(getCapacityFactory());
+		server = bigBlueButtonManager.updateServer(server);
+		fireEvent(ureq, Event.DONE_EVENT);
+	}
+
+	@Override
+	protected void formCancelled(UserRequest ureq) {
+		fireEvent(ureq, Event.CANCELLED_EVENT);
+	}
+
+	private void doCheckConnection() {
+		BigBlueButtonErrors errors = new BigBlueButtonErrors();
+		boolean loginOk = checkConnection(errors);
+		if(errors.hasErrors()) {
+			getWindowControl().setError(BigBlueButtonErrorHelper.formatErrors(getTranslator(), errors));
+		} else if(loginOk) {
+			showInfo("connection.successful");
+		} else {
+			showError("connection.failed");
+		}
+	}
+	
+	private boolean checkConnection(BigBlueButtonErrors errors) {
+		String url = urlEl.getValue();
+		String sharedSecret = sharedSecretEl.getValue();
+		if(PLACEHOLDER.equals(sharedSecret)) {
+			if(StringHelper.containsNonWhitespace(replacedSharedSecretValue)) {
+				sharedSecret = replacedSharedSecretValue;
+			} else if(server != null) {
+				sharedSecret = server.getSharedSecret();
+			}
+		} else {
+			replacedSharedSecretValue = sharedSecret;
+			sharedSecretEl.setValue(PLACEHOLDER);
+		}
+		return bigBlueButtonManager.checkConnection(url, sharedSecret, errors);
+	}
+}
diff --git a/src/main/java/org/olat/modules/bigbluebutton/ui/_content/confirm_delete_server.html b/src/main/java/org/olat/modules/bigbluebutton/ui/_content/confirm_delete_server.html
new file mode 100644
index 0000000000000000000000000000000000000000..4b4a2d1b9c0ee54698d07a19ada4d418f51c5fe2
--- /dev/null
+++ b/src/main/java/org/olat/modules/bigbluebutton/ui/_content/confirm_delete_server.html
@@ -0,0 +1,8 @@
+<div class="o_warning" role="alert">
+	<i class="o_icon o_icon-lg o_icon_important"> </i> $msg
+</div>
+
+<div class="o_button_group">
+	$r.render("cancel")
+	$r.render("delete")
+</div>
\ No newline at end of file
diff --git a/src/main/java/org/olat/modules/bigbluebutton/ui/_i18n/LocalStrings_de.properties b/src/main/java/org/olat/modules/bigbluebutton/ui/_i18n/LocalStrings_de.properties
index 0e0a528172e430b9b24d9593b5a272f219a85b7e..70606ea5a4cc1ec8e12284887cccaa9dc97f100a 100644
--- a/src/main/java/org/olat/modules/bigbluebutton/ui/_i18n/LocalStrings_de.properties
+++ b/src/main/java/org/olat/modules/bigbluebutton/ui/_i18n/LocalStrings_de.properties
@@ -3,6 +3,7 @@ account.configuration=Konfiguration
 add.daily.meeting=T\u00E4glich wiederkehrende Online-Termin hinzuf\u00FCgen
 add.meeting=Online-Termin hinzuf\u00FCgen
 add.permanent.meeting=Online-Termin ohne Datum hinzuf\u00FCgen
+add.server=Server hinzuf\u00FCgen
 add.single.meeting=Einzige Online-Termin hinzuf\u00FCgen
 add.template=Raumvorlage erstellen
 add.weekly.meeting=W\u00F6chentlich wiederkehrende Online-Termin hinzuf\u00FCgen
@@ -13,6 +14,8 @@ bigbluebutton.module.enabled=Modul "BigBlueButton"
 bigbluebutton.module.enabled.for=Aktivieren f\u00FCr
 bigbluebutton.module.enabled.for.courses=Kurse
 bigbluebutton.module.enabled.for.groups=Gruppen
+bigbluebutton.servers=Server
+bigbluebutton.servers.empty=Sie haben noch kein Server konfiguriert
 bigbluebutton.title=Konfiguration BigBlueButton Web Conferencing Service
 calendar.open=Raumbuchungen anzeigen
 calendar.title=Kalender
@@ -21,19 +24,24 @@ confirm.delete.meeting=Wollen Sie wirklich den Online-Termin "{0}" l\u00F6schen?
 confirm.delete.meeting.title=Online-Termin "{0}" l\u00F6schen
 confirm.delete.meetings=Wollen Sie wirklich {0} Online-Termine ({1}) l\u00F6schen?
 confirm.delete.meetings.title={0} Online-Termine l\u00F6schen
+confirm.delete.server=Wollen Sie wirklich den Server "{0}" l\u00F6schen? <strong>Alle Meetings und Aufzeichnungen werden auch gel\u00F6scht.</strong>
+confirm.delete.server.title=Server "{0}" l\u00F6schen
 confirm.delete.template=Wollen Sie wirklich die Raumvorlage "{0}" l\u00F6schen?
 confirm.delete.template.title=Raumvorlage "{0}" l\u00F6schen
 connection.failed=BigBlueButton API Login fehlgeschlagen.
 connection.successful=BigBlueButton API Login erfolgreich\!
+edit.server=Server "{0}" bearbeiten
 edit.template=Raumvorlage "{0}" bearbeiten
 enable.permanent.meeting=Online-Termine ohne Datum
 enable.adhoc.meeting=Adhoc Online-Termin
+error.capacity.factory=Ein Nummer zwischen 1.0 und 100.0
 error.connectionValidationFailed=Die Verbindungspr\u00fcfung ist fehlgeschlagen: <pre>{0}</pre>
 error.date.in.past=Der Termin kann sich nicht in der Vergangenheit befinden.
 error.duration=Termindauer \u00fcberschritten. Maximal Dauer: {0} Minuten.
 error.end.past=Der Online-Termin kann nicht in Vergangenheit geplant werden.
 error.prefix=Ein Fehler ist aufgetreten\:
 error.same.day=Sie haben schon ein Meeting an diesem Tag geplant.
+error.server.exists=Ein Server mit diesem URL existiert schon.
 error.server.raw={1} <small>Schl\u00FCssel\: {0}</small>
 error.start.after.end=Das Enddatum darf nicht vor dem Beginndatum sein.
 error.too.long.time=Zeit ist zu lang. Es sind maximal {0} Minuten erlaubt.
@@ -71,6 +79,10 @@ option.baseurl=BigBlueButton API URL
 option.baseurl.example=https\://bigbluebutton.openolat.com/bigbluebutton/
 option.bigbluebutton.secret=Secret
 option.bigbluebutton.shared.secret=Shared secret
+option.capacity.factory=Capacity factor
+option.capacity.factor.example=Ein Nummer zwischen 1 und 100 (zum Beispiel 1.23)
+option.enabled.server=Server aktivieren
+option.recordingurl=Aufzeichnung URL
 recordings=Aufzeichnungen
 recording.browser.infos=Aufzeichnungen k\u00F6nnen nur mit Google Chrome und Firefox gesehen werden.
 recording.type.podcast=Podcast
@@ -81,6 +93,7 @@ role.coach=Betreuer
 role.owner=Kursbesitzer
 role.group=Gruppenmitglied
 server.overloaded=F\u00FCr das gew\u00E4hlten Datum/Uhrzeit ist kein Raum verf\u00FCgbar. W\u00E4hlen Sie ein anderes Datum/Uhrzeit oder eine andere Raumvorlage.
+servers.title=Server
 table.header.day.week=Tag
 table.header.available=Verf\u00FCgbarkeit
 table.header.enabled=Aktiv
@@ -94,6 +107,9 @@ table.header.recording.type=Typ
 table.header.recording.open=\u00D6ffnen
 table.header.recording.start=Beginn
 table.header.recording.end=Ende
+table.header.server.enabled=Eingeschaltet
+table.header.server.recording=Aufzeichnungen URL
+table.header.server.url=URL
 table.header.template=Raumvorlage
 table.header.webcams.only.moderator=Nur Moderatorenkamera
 template.allowModsToUnmuteUsers=Moderatoren d\u00FCrfen Teilnehmer-Mikrofon aktiveren
diff --git a/src/main/java/org/olat/modules/bigbluebutton/ui/_i18n/LocalStrings_en.properties b/src/main/java/org/olat/modules/bigbluebutton/ui/_i18n/LocalStrings_en.properties
index 9366f77bcd070620138ecc8165e9ac01e3af4961..2050e895532ff1748cd15a4ac63effb99c956f3a 100644
--- a/src/main/java/org/olat/modules/bigbluebutton/ui/_i18n/LocalStrings_en.properties
+++ b/src/main/java/org/olat/modules/bigbluebutton/ui/_i18n/LocalStrings_en.properties
@@ -13,6 +13,8 @@ bigbluebutton.module.enabled=Module "BigBlueButton"
 bigbluebutton.module.enabled.for=Activate for
 bigbluebutton.module.enabled.for.courses=Courses
 bigbluebutton.module.enabled.for.groups=Groups
+bigbluebutton.servers=Servers
+bigbluebutton.servers.empty=You don't have configured a server. 
 bigbluebutton.title=Configuration of BigBlueButton Web Conferencing service
 calendar.open=Show room bookings
 calendar.title=Calendar
@@ -21,19 +23,24 @@ confirm.delete.meeting=Do you really want to delete the online-meeting "{0}"?
 confirm.delete.meeting.title=Delete online-meeting "{0}"
 confirm.delete.meetings=Do you really want to delete the {0} online-meetings ({1})?
 confirm.delete.meetings.title=Delete {0} online-meetings
+confirm.delete.server=Do you really want to delete the server "{0}"? <strong>All meetings and recordings will be deleted too.</strong>
+confirm.delete.server.title=Delete server "{0}"
 confirm.delete.template=Do you really want to delete the room-template "{0}"?
 confirm.delete.template.title=Delete room-template "{0}"
 connection.failed=BigBlueButton API login failed.
 connection.successful=BigBlueButton API login successful\!
+edit.server=Edit server "{0}"
 edit.template=Edit room-template "{0}"
 enable.permanent.meeting=Online-Meetings without date
 enable.adhoc.meeting=Adhoc online-meeting
+error.capacity.factory=A number between 1.0 and 100.0
 error.connectionValidationFailed=The connection validation failed: <pre>{0}</pre>
 error.date.in.past=The meeting date can not be in the past.
 error.duration=Meeting duration is too long. Maximal duration: {0} minutes.
 error.end.past=Online-meeting cannot be planned in the past.
 error.prefix=An error happened\:
 error.same.day=You already have a meeting planed at this date.
+error.server.exists=A server with this URL already exists.
 error.server.raw={1} <small>Key {0}</small>
 error.start.after.end=The end date must not be before the start date.
 error.too.long.time=Time is too long. It is limited to {0} minutes.
@@ -71,6 +78,10 @@ option.baseurl=BigBlueButton API URL
 option.baseurl.example=https\://bigbluebutton.openolat.com/bigbluebutton/
 option.bigbluebutton.secret=Secret
 option.bigbluebutton.shared.secret=Shared secret
+option.capacity.factory=Capacity factor
+option.capacity.factor.example=A number between 1 and 100 (for example: 1.23)
+option.enabled.server=Activate server
+option.recordingurl=Recording URL
 recordings=Recordings
 recording.browser.infos=Recordings can only be viewed with Google Chrome or Firefox.
 recording.type.podcast=Podcast
@@ -81,6 +92,7 @@ role.coach=Coach
 role.owner=Course owner
 role.group=Group user
 server.overloaded=There is no room available for the choosen date/time. Choose another date/time or another room template.
+servers.title=Servers
 table.header.day.week=Day
 table.header.enabled=Enabled
 table.header.permanent=Withour date
@@ -93,6 +105,9 @@ table.header.recording.type=Type
 table.header.recording.open=Open
 table.header.recording.start=Start
 table.header.recording.end=End
+table.header.server.enabled=Enabled
+table.header.server.recording=Recording URL
+table.header.server.url=URL
 table.header.template=Room-template
 table.header.webcams.only.moderator=Webcams only for moderators
 template.allowModsToUnmuteUsers=Allow moderators to activate the participants microphone
diff --git a/src/main/java/org/olat/upgrade/OLATUpgrade_14_2_7.java b/src/main/java/org/olat/upgrade/OLATUpgrade_14_2_7.java
new file mode 100644
index 0000000000000000000000000000000000000000..02c80b0d500119d03c3ca9953e03f212125a2c84
--- /dev/null
+++ b/src/main/java/org/olat/upgrade/OLATUpgrade_14_2_7.java
@@ -0,0 +1,143 @@
+/**
+ * <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.upgrade;
+
+import java.net.URI;
+import java.util.Date;
+import java.util.List;
+
+import javax.persistence.TemporalType;
+
+import org.apache.logging.log4j.Logger;
+import org.olat.core.commons.persistence.DB;
+import org.olat.core.commons.persistence.QueryBuilder;
+import org.olat.core.logging.Tracing;
+import org.olat.modules.bigbluebutton.BigBlueButtonManager;
+import org.olat.modules.bigbluebutton.BigBlueButtonMeeting;
+import org.olat.modules.bigbluebutton.BigBlueButtonModule;
+import org.olat.modules.bigbluebutton.BigBlueButtonServer;
+import org.olat.modules.bigbluebutton.model.BigBlueButtonMeetingImpl;
+import org.springframework.beans.factory.annotation.Autowired;
+
+/**
+ * 
+ * Initial date: 2 avr. 2020<br>
+ * @author srosse, stephane.rosse@frentix.com, http://www.frentix.com
+ *
+ */
+public class OLATUpgrade_14_2_7 extends OLATUpgrade {
+
+	private static final Logger log = Tracing.createLoggerFor(OLATUpgrade_14_2_7.class);
+
+	private static final String VERSION = "OLAT_14.2.7";
+	private static final String BIGBLUEBUTTON_TO_DB = "BIGBLUEBUTTON SERVER TO DB";
+	
+	@Autowired
+	private DB dbInstance;
+	@Autowired
+	private BigBlueButtonModule bigBlueButtonModule;
+	@Autowired
+	private BigBlueButtonManager bigBlueButtonManager;
+
+	public OLATUpgrade_14_2_7() {
+		super();
+	}
+	
+	@Override
+	public String getVersion() {
+		return VERSION;
+	}
+
+	@Override
+	public boolean doPostSystemInitUpgrade(UpgradeManager upgradeManager) {
+		UpgradeHistoryData uhd = upgradeManager.getUpgradesHistory(VERSION);
+		if (uhd == null) {
+			// has never been called, initialize
+			uhd = new UpgradeHistoryData();
+		} else if (uhd.isInstallationComplete()) {
+			return false;
+		}
+		
+		boolean allOk = true;
+		allOk &= migrateBigBlueButtonServer(upgradeManager, uhd);
+
+		uhd.setInstallationComplete(allOk);
+		upgradeManager.setUpgradesHistory(uhd, VERSION);
+		if(allOk) {
+			log.info(Tracing.M_AUDIT, "Finished OLATUpgrade_14_2_7 successfully!");
+		} else {
+			log.info(Tracing.M_AUDIT, "OLATUpgrade_14_2_67not finished, try to restart OpenOlat!");
+		}
+		return allOk;
+	}
+	
+	private boolean migrateBigBlueButtonServer(UpgradeManager upgradeManager, UpgradeHistoryData uhd) {
+		boolean allOk = true;
+		if (!uhd.getBooleanDataValue(BIGBLUEBUTTON_TO_DB)) {
+			
+			if(bigBlueButtonModule.isEnabled()) {
+				URI uri = bigBlueButtonModule.getBigBlueButtonURI();
+				String sharedSecret = bigBlueButtonModule.getSharedSecret();
+				if(uri != null && sharedSecret != null) {
+					String uriStr = uri.toString();
+					BigBlueButtonServer server = null;;
+					if(!bigBlueButtonManager.hasServer(uriStr)) {
+						server = bigBlueButtonManager.createServer(uriStr, null, sharedSecret);
+						dbInstance.commitAndCloseSession();
+					} else {
+						List<BigBlueButtonServer> servers = bigBlueButtonManager.getServers();
+						for(BigBlueButtonServer potentialServer:servers) {
+							if(uriStr.startsWith(potentialServer.getUrl()) || potentialServer.getUrl().startsWith(uriStr)) {
+								server = potentialServer;
+							}
+						}
+					}
+					if(server != null) {
+						migrateBigBlueButtonServer(server);
+						dbInstance.commitAndCloseSession();
+					}
+				}
+			}
+
+			uhd.setBooleanDataValue(BIGBLUEBUTTON_TO_DB, allOk);
+			upgradeManager.setUpgradesHistory(uhd, VERSION);
+		}
+		return allOk;
+	}
+	
+	private void migrateBigBlueButtonServer(BigBlueButtonServer server) {
+		List<BigBlueButtonMeeting> meetings = getMeetingsToMigrate();
+		for(BigBlueButtonMeeting meeting:meetings) {
+			((BigBlueButtonMeetingImpl)meeting).setServer(server);
+			bigBlueButtonManager.updateMeeting(meeting);
+		}
+	}
+	
+	private List<BigBlueButtonMeeting> getMeetingsToMigrate() {
+		QueryBuilder sb = new QueryBuilder();
+		sb.append("select meeting from bigbluebuttonmeeting as meeting")
+		  .append(" where meeting.server.key is null and (meeting.permanent=true or meeting.startDate<=:now)");
+		
+		return dbInstance.getCurrentEntityManager()
+				.createQuery(sb.toString(), BigBlueButtonMeeting.class)
+				.setParameter("now", new Date(), TemporalType.TIMESTAMP)
+				.getResultList();
+	}
+}
diff --git a/src/main/java/org/olat/upgrade/_spring/upgradeContext.xml b/src/main/java/org/olat/upgrade/_spring/upgradeContext.xml
index e7f846dafd6c5113dde494eabaa7a0270d61a256..daca3a1c382b6a9043006f46d0d8dd8f95fb5a11 100644
--- a/src/main/java/org/olat/upgrade/_spring/upgradeContext.xml
+++ b/src/main/java/org/olat/upgrade/_spring/upgradeContext.xml
@@ -55,6 +55,7 @@
 				<bean id="upgrade_14_1_0" class="org.olat.upgrade.OLATUpgrade_14_1_0"/>
 				<bean id="upgrade_14_2_0" class="org.olat.upgrade.OLATUpgrade_14_2_0"/>
 				<bean id="upgrade_14_2_6" class="org.olat.upgrade.OLATUpgrade_14_2_6"/>
+				<bean id="upgrade_14_2_7" class="org.olat.upgrade.OLATUpgrade_14_2_7"/>
 			</list>
 		</property>
 	</bean>
diff --git a/src/main/resources/META-INF/persistence.xml b/src/main/resources/META-INF/persistence.xml
index f0e5e70a1cfda75776b73d864f8a81bb8af5578d..2ae89276644d1f0b679ed406b41e8e24c71944cb 100644
--- a/src/main/resources/META-INF/persistence.xml
+++ b/src/main/resources/META-INF/persistence.xml
@@ -175,6 +175,7 @@
 		<class>org.olat.modules.assessment.model.AssessmentEntryImpl</class>
 		<class>org.olat.modules.bigbluebutton.model.BigBlueButtonMeetingImpl</class>
 		<class>org.olat.modules.bigbluebutton.model.BigBlueButtonMeetingTemplateImpl</class>
+		<class>org.olat.modules.bigbluebutton.model.BigBlueButtonServerImpl</class>
 		<class>org.olat.modules.curriculum.model.CurriculumImpl</class>
 		<class>org.olat.modules.curriculum.model.CurriculumElementImpl</class>
 		<class>org.olat.modules.curriculum.model.CurriculumElementTypeImpl</class>
diff --git a/src/main/resources/database/mysql/alter_14_2_x_to_14_2_7.sql b/src/main/resources/database/mysql/alter_14_2_x_to_14_2_7.sql
new file mode 100644
index 0000000000000000000000000000000000000000..df320052edca28f728d6fad2616964d3949a94d4
--- /dev/null
+++ b/src/main/resources/database/mysql/alter_14_2_x_to_14_2_7.sql
@@ -0,0 +1,18 @@
+create table o_bbb_server (
+   id bigint not null auto_increment,
+   creationdate datetime not null,
+   lastmodified datetime not null,
+   b_name varchar(128),
+   b_url varchar(255) not null,
+   b_shared_secret varchar(255),
+   b_recording_url varchar(255),
+   b_enabled bool default true,
+   b_capacity_factor decimal,
+   primary key (id)
+);
+alter table o_bbb_server ENGINE = InnoDB;
+
+alter table o_bbb_meeting add column fk_server_id bigint;
+
+alter table o_bbb_meeting add constraint bbb_meet_serv_idx foreign key (fk_server_id) references o_bbb_server (id);
+
diff --git a/src/main/resources/database/mysql/setupDatabase.sql b/src/main/resources/database/mysql/setupDatabase.sql
index d29684418a72a1e645cfb682a41fe7d598cc0c6e..16eea6d77e6ce4766c240f2777f79396cb79530e 100644
--- a/src/main/resources/database/mysql/setupDatabase.sql
+++ b/src/main/resources/database/mysql/setupDatabase.sql
@@ -1251,6 +1251,19 @@ create table o_bbb_template (
    primary key (id)
 );
 
+create table o_bbb_server (
+   id bigint not null auto_increment,
+   creationdate datetime not null,
+   lastmodified datetime not null,
+   b_name varchar(128),
+   b_url varchar(255) not null,
+   b_shared_secret varchar(255),
+   b_recording_url varchar(255),
+   b_enabled bool default true,
+   b_capacity_factor decimal,
+   primary key (id)
+);
+
 create table o_bbb_meeting (
    id bigint not null auto_increment,
    creationdate datetime not null,
@@ -1272,6 +1285,7 @@ create table o_bbb_meeting (
    a_sub_ident varchar(64) default null,
    fk_group_id bigint default null,
    fk_template_id bigint default null,
+   fk_server_id bigint,
    primary key (id)
 );
 
@@ -3299,6 +3313,7 @@ alter table o_aconnect_meeting ENGINE = InnoDB;
 alter table o_aconnect_user ENGINE = InnoDB;
 alter table o_bbb_template ENGINE = InnoDB;
 alter table o_bbb_meeting ENGINE = InnoDB;
+alter table o_bbb_server ENGINE = InnoDB;
 alter table o_im_message ENGINE = InnoDB;
 alter table o_im_notification ENGINE = InnoDB;
 alter table o_im_roster_entry ENGINE = InnoDB;
@@ -3686,6 +3701,8 @@ alter table o_bbb_meeting add constraint bbb_meet_entry_idx foreign key (fk_entr
 alter table o_bbb_meeting add constraint bbb_meet_grp_idx foreign key (fk_group_id) references o_gp_business (group_id);
 alter table o_bbb_meeting add constraint bbb_meet_template_idx foreign key (fk_template_id) references o_bbb_template (id);
 
+alter table o_bbb_meeting add constraint bbb_meet_serv_idx foreign key (fk_server_id) references o_bbb_server (id);
+
 -- eportfolio
 alter table o_ep_artefact add constraint FKF26C8375236F28X foreign key (fk_artefact_auth_id) references o_bs_identity (id);
 alter table o_ep_artefact add constraint FKA0070D12316A97B4 foreign key (fk_struct_el_id) references o_ep_struct_el (structure_id);
diff --git a/src/main/resources/database/oracle/alter_14_2_x_to_14_2_7.sql b/src/main/resources/database/oracle/alter_14_2_x_to_14_2_7.sql
new file mode 100644
index 0000000000000000000000000000000000000000..6fd06102c8060c777afbd8130c7193405263a908
--- /dev/null
+++ b/src/main/resources/database/oracle/alter_14_2_x_to_14_2_7.sql
@@ -0,0 +1,18 @@
+create table o_bbb_server (
+   id number(20) generated always as identity,
+   creationdate timestamp not null,
+   lastmodified timestamp not null,
+   b_name varchar(128),
+   b_url varchar(255) not null,
+   b_shared_secret varchar(255),
+   b_recording_url varchar(255),
+   b_enabled number default 1 not null,
+   b_capacity_factor decimal,
+   primary key (id)
+);
+
+alter table o_bbb_meeting add fk_server_id number(20);
+
+alter table o_bbb_meeting add constraint bbb_meet_serv_idx foreign key (fk_server_id) references o_bbb_server (id);
+create index idx_bbb_meet_serv_idx on o_bbb_meeting(fk_server_id);
+
diff --git a/src/main/resources/database/oracle/setupDatabase.sql b/src/main/resources/database/oracle/setupDatabase.sql
index 064778b45c763db3f4d8de0f5e8a87a087849d75..99113c672d9affcbbcbddcdd13b912297e2d93a3 100644
--- a/src/main/resources/database/oracle/setupDatabase.sql
+++ b/src/main/resources/database/oracle/setupDatabase.sql
@@ -1317,6 +1317,19 @@ create table o_bbb_template (
    primary key (id)
 );
 
+create table o_bbb_server (
+   id number(20) generated always as identity,
+   creationdate timestamp not null,
+   lastmodified timestamp not null,
+   b_name varchar(128),
+   b_url varchar(255) not null,
+   b_shared_secret varchar(255),
+   b_recording_url varchar(255),
+   b_enabled number default 1 not null,
+   b_capacity_factor decimal,
+   primary key (id)
+);
+
 create table o_bbb_meeting (
    id number(20) generated always as identity,
    creationdate timestamp not null,
@@ -1338,6 +1351,7 @@ create table o_bbb_meeting (
    a_sub_ident varchar(64) default null,
    fk_group_id number(20) default null,
    fk_template_id number(20) default null,
+   fk_server_id number(20),
    primary key (id)
 );
 
@@ -3718,6 +3732,8 @@ alter table o_bbb_meeting add constraint bbb_meet_grp_idx foreign key (fk_group_
 create index idx_bbb_meet_grp_idx on o_bbb_meeting(fk_group_id);
 alter table o_bbb_meeting add constraint bbb_meet_template_idx foreign key (fk_template_id) references o_bbb_template (id);
 create index idx_bbb_meet_template_idx on o_bbb_meeting(fk_template_id);
+alter table o_bbb_meeting add constraint bbb_meet_serv_idx foreign key (fk_server_id) references o_bbb_server (id);
+create index idx_bbb_meet_serv_idx on o_bbb_meeting(fk_server_id);
 
 -- eportfolio
 alter table o_ep_artefact add constraint FKF26C8375236F28X foreign key (fk_artefact_auth_id) references o_bs_identity (id);
diff --git a/src/main/resources/database/postgresql/alter_14_2_x_to_14_2_7.sql b/src/main/resources/database/postgresql/alter_14_2_x_to_14_2_7.sql
new file mode 100644
index 0000000000000000000000000000000000000000..ebb7cc0ce3639be9b3fddd33bf8cbee2462f2de6
--- /dev/null
+++ b/src/main/resources/database/postgresql/alter_14_2_x_to_14_2_7.sql
@@ -0,0 +1,18 @@
+create table o_bbb_server (
+   id bigserial,
+   creationdate timestamp not null,
+   lastmodified timestamp not null,
+   b_name varchar(128),
+   b_url varchar(255) not null,
+   b_shared_secret varchar(255),
+   b_recording_url varchar(255),
+   b_enabled bool default true,
+   b_capacity_factor decimal,
+   primary key (id)
+);
+
+alter table o_bbb_meeting add column fk_server_id bigint;
+
+alter table o_bbb_meeting add constraint bbb_meet_serv_idx foreign key (fk_server_id) references o_bbb_server (id);
+create index idx_bbb_meet_serv_idx on o_bbb_meeting(fk_server_id);
+
diff --git a/src/main/resources/database/postgresql/setupDatabase.sql b/src/main/resources/database/postgresql/setupDatabase.sql
index 7af79c0ef67d11304323baf9a7fc8cda05556f00..ef7899ac4ffbeac4e908c55395ba05abe940d985 100644
--- a/src/main/resources/database/postgresql/setupDatabase.sql
+++ b/src/main/resources/database/postgresql/setupDatabase.sql
@@ -1276,6 +1276,19 @@ create table o_bbb_template (
    primary key (id)
 );
 
+create table o_bbb_server (
+   id bigserial,
+   creationdate timestamp not null,
+   lastmodified timestamp not null,
+   b_name varchar(128),
+   b_url varchar(255) not null,
+   b_shared_secret varchar(255),
+   b_recording_url varchar(255),
+   b_enabled bool default true,
+   b_capacity_factor decimal,
+   primary key (id)
+);
+
 create table o_bbb_meeting (
    id bigserial,
    creationdate timestamp not null,
@@ -1297,6 +1310,7 @@ create table o_bbb_meeting (
    a_sub_ident varchar(64) default null,
    fk_group_id int8 default null,
    fk_template_id int8 default null,
+   fk_server_id int8 default null,
    primary key (id)
 );
 
@@ -3606,6 +3620,8 @@ alter table o_bbb_meeting add constraint bbb_meet_grp_idx foreign key (fk_group_
 create index idx_bbb_meet_grp_idx on o_bbb_meeting(fk_group_id);
 alter table o_bbb_meeting add constraint bbb_meet_template_idx foreign key (fk_template_id) references o_bbb_template (id);
 create index idx_bbb_meet_template_idx on o_bbb_meeting(fk_template_id);
+alter table o_bbb_meeting add constraint bbb_meet_serv_idx foreign key (fk_server_id) references o_bbb_server (id);
+create index idx_bbb_meet_serv_idx on o_bbb_meeting(fk_server_id);
 
 -- eportfolio
 alter table o_ep_artefact add constraint FKF26C8375236F28X foreign key (fk_artefact_auth_id) references o_bs_identity (id);
diff --git a/src/test/java/org/olat/modules/bigbluebutton/manager/BigBlueButtonMeetingDAOTest.java b/src/test/java/org/olat/modules/bigbluebutton/manager/BigBlueButtonMeetingDAOTest.java
index 12e6947fd801ec0f78a97f851547cd852a016ac9..895e89f9ead10e2ffd61fc9ea91415757760ba7f 100644
--- a/src/test/java/org/olat/modules/bigbluebutton/manager/BigBlueButtonMeetingDAOTest.java
+++ b/src/test/java/org/olat/modules/bigbluebutton/manager/BigBlueButtonMeetingDAOTest.java
@@ -32,6 +32,8 @@ import org.olat.group.BusinessGroup;
 import org.olat.group.manager.BusinessGroupDAO;
 import org.olat.modules.bigbluebutton.BigBlueButtonMeeting;
 import org.olat.modules.bigbluebutton.BigBlueButtonMeetingTemplate;
+import org.olat.modules.bigbluebutton.BigBlueButtonServer;
+import org.olat.modules.bigbluebutton.model.BigBlueButtonMeetingImpl;
 import org.olat.repository.RepositoryEntry;
 import org.olat.test.JunitTestHelper;
 import org.olat.test.OlatTestCase;
@@ -50,6 +52,8 @@ public class BigBlueButtonMeetingDAOTest extends OlatTestCase {
 	@Autowired
 	private BusinessGroupDAO businessGroupDao;
 	@Autowired
+	private BigBlueButtonServerDAO bigBlueButtonServerDao;
+	@Autowired
 	private BigBlueButtonMeetingDAO bigBlueButtonMeetingDao;
 	@Autowired
 	private BigBlueButtonMeetingTemplateDAO bigBlueButtonMeetingTemplateDao;
@@ -118,6 +122,21 @@ public class BigBlueButtonMeetingDAOTest extends OlatTestCase {
 		Assert.assertNull(reloadedMeeting.getBusinessGroup());
 	}
 	
+	@Test
+	public void loadForUpdate() {
+		RepositoryEntry entry = JunitTestHelper.createAndPersistRepositoryEntry();
+		String name = "BigBlueButton - 2";
+		String subIdent = UUID.randomUUID().toString();
+		
+		BigBlueButtonMeeting meeting = bigBlueButtonMeetingDao.createAndPersistMeeting(name, entry, subIdent, null);
+		dbInstance.commit();
+
+		
+		BigBlueButtonMeeting reloadedMeeting = bigBlueButtonMeetingDao.loadForUpdate(meeting);
+		dbInstance.commit();
+		Assert.assertNotNull(reloadedMeeting);
+	}
+	
 	@Test
 	public void getMeetingsByRepositoryEntry() {
 		RepositoryEntry entry = JunitTestHelper.createAndPersistRepositoryEntry();
@@ -146,6 +165,27 @@ public class BigBlueButtonMeetingDAOTest extends OlatTestCase {
 		Assert.assertTrue(meetings.contains(meeting));
 	}
 	
+	@Test
+	public void getMeetingsByServer() {
+		String url = "https://bbb.frentix.com/bigbluebutton";
+		String sharedSecret = UUID.randomUUID().toString();
+		BigBlueButtonServer server = bigBlueButtonServerDao.createServer(url, null, sharedSecret);
+		
+		String name = "BigBlueButton - 7";
+		BusinessGroup group = businessGroupDao.createAndPersist(null, "BBB server", "bbb-server", -1, -1, false, false, false, false, false);
+		BigBlueButtonMeeting meeting = bigBlueButtonMeetingDao.createAndPersistMeeting(name, null, null, group);
+		dbInstance.commit();
+		
+		((BigBlueButtonMeetingImpl)meeting).setServer(server);
+		meeting = bigBlueButtonMeetingDao.updateMeeting(meeting);
+		dbInstance.commitAndCloseSession();
+		
+		List<BigBlueButtonMeeting> serversMeetings = bigBlueButtonMeetingDao.getMeetings(server);
+		Assert.assertNotNull(serversMeetings);
+		Assert.assertEquals(1, serversMeetings.size());
+		Assert.assertTrue(serversMeetings.contains(meeting));
+	}
+	
 	@Test
 	public void getConcurrentMeetings() {
 		String externalId = UUID.randomUUID().toString();
diff --git a/src/test/java/org/olat/modules/bigbluebutton/manager/BigBlueButtonServerDAOTest.java b/src/test/java/org/olat/modules/bigbluebutton/manager/BigBlueButtonServerDAOTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..4116106e0914bb24550ba4911797f25bcccd94b5
--- /dev/null
+++ b/src/test/java/org/olat/modules/bigbluebutton/manager/BigBlueButtonServerDAOTest.java
@@ -0,0 +1,76 @@
+/**
+ * <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.modules.bigbluebutton.manager;
+
+import java.util.List;
+import java.util.UUID;
+
+import org.junit.Assert;
+import org.junit.Test;
+import org.olat.core.commons.persistence.DB;
+import org.olat.modules.bigbluebutton.BigBlueButtonServer;
+import org.olat.test.OlatTestCase;
+import org.springframework.beans.factory.annotation.Autowired;
+
+/**
+ * 
+ * Initial date: 7 avr. 2020<br>
+ * @author srosse, stephane.rosse@frentix.com, http://www.frentix.com
+ *
+ */
+public class BigBlueButtonServerDAOTest extends OlatTestCase {
+	
+	@Autowired
+	private DB dbInstance;
+	@Autowired
+	private BigBlueButtonServerDAO bigBlueButtonServerDao;
+	
+	@Test
+	public void createServer() {
+		String url = "https://" + UUID.randomUUID().toString() + "/bigbluebutton";
+		String recordingUrl = "https://" + UUID.randomUUID().toString() + "/bigbluebutton/recordings";
+		String sharedSecret = UUID.randomUUID().toString();
+		
+		BigBlueButtonServer server = bigBlueButtonServerDao.createServer(url, recordingUrl, sharedSecret);
+		dbInstance.commitAndCloseSession();
+		
+		Assert.assertNotNull(server);
+		Assert.assertEquals(url, server.getUrl());
+		Assert.assertEquals(recordingUrl, server.getRecordingUrl());
+		Assert.assertEquals(sharedSecret, server.getSharedSecret());
+	}
+	
+	@Test
+	public void getServers() {
+		String url = "https://" + UUID.randomUUID().toString() + "/bigbluebutton";
+		String recordingUrl = "https://" + UUID.randomUUID().toString() + "/bigbluebutton/recordings";
+		String sharedSecret = UUID.randomUUID().toString();
+		
+		BigBlueButtonServer server = bigBlueButtonServerDao.createServer(url, recordingUrl, sharedSecret);
+		dbInstance.commitAndCloseSession();
+		
+		List<BigBlueButtonServer> servers = bigBlueButtonServerDao.getServers();
+		
+		Assert.assertNotNull(servers);
+		Assert.assertTrue(servers.contains(server));
+	}
+	
+
+}
diff --git a/src/test/java/org/olat/test/AllTestsJunit4.java b/src/test/java/org/olat/test/AllTestsJunit4.java
index 0c1d58c88cec4d19a8d7c5bc4555b970e1680b38..fcbbdfe5ceb7e6bce31374dcd24b17986c9e13e6 100644
--- a/src/test/java/org/olat/test/AllTestsJunit4.java
+++ b/src/test/java/org/olat/test/AllTestsJunit4.java
@@ -200,6 +200,7 @@ import org.junit.runners.Suite;
 	org.olat.modules.adobeconnect.manager.AdobeConnectUserDAOTest.class,
 	org.olat.modules.adobeconnect.manager.AdobeConnectMeetingDAOTest.class,
 	org.olat.modules.adobeconnect.manager.AdobeConnectUtilsTest.class,
+	org.olat.modules.bigbluebutton.manager.BigBlueButtonServerDAOTest.class,
 	org.olat.modules.bigbluebutton.manager.BigBlueButtonMeetingDAOTest.class,
 	org.olat.modules.bigbluebutton.manager.BigBlueButtonMeetingTemplateDAOTest.class,
 	org.olat.modules.bigbluebutton.manager.BigBlueButtonUriBuilderTest.class,