From 40a802e74de433b7fc372ee81a804b89c7e7e820 Mon Sep 17 00:00:00 2001
From: fkiefer <none@none>
Date: Tue, 21 Mar 2017 11:14:35 +0100
Subject: [PATCH] OO-2621 Notification messages in groups, refactor group
 information tool

---
 .../collaboration/CollaborationTools.java     |  45 +++
 .../CollaborationToolsSettingsController.java |   8 +-
 .../collaboration/NewsFormController.java     |  70 ++--
 .../_i18n/LocalStrings_de.properties          |   1 +
 .../_i18n/LocalStrings_en.properties          |   1 +
 .../manager/InfoMessageFrontendManager.java   |   9 +-
 .../InfoMessageFrontendManagerImpl.java       |  38 +-
 .../info/manager/InfoMessageManager.java      |   4 +
 .../info/manager/InfoMessageManagerImpl.java  |  49 ++-
 .../commons/info/model/InfoMessageImpl.java   |  85 ++++-
 .../InfoMessageNotificationHandler.java       |  26 +-
 .../notification/InfoSubscriptionManager.java |   3 +
 .../InfoSubscriptionManagerImpl.java          |   5 +
 .../_i18n/LocalStrings_de.properties          |   1 +
 .../_i18n/LocalStrings_en.properties          |   1 +
 .../info/ui/InfoDisplayController.java        |  19 +
 .../info/ui/SendInfoMailFormatter.java}       |  13 +-
 .../course/nodes/info/InfoRunController.java  |   3 +-
 .../manager/BusinessGroupServiceImpl.java     |  11 +-
 .../run/BusinessGroupMainRunController.java   |   2 +-
 .../group/ui/run/InfoGroupRunController.java  | 214 +++++++++++
 .../ui/run/SendGroupMembersMailOption.java    |  64 ++++
 .../org/olat/group/ui/run/_content/run.html   |   6 +
 .../ui/run/_i18n/LocalStrings_de.properties   |   4 +
 .../ui/run/_i18n/LocalStrings_en.properties   |   4 +
 .../org/olat/search/_spring/searchContext.xml |   4 +
 .../indexer/group/GroupInfoIndexer.java       |  99 +++++
 .../org/olat/upgrade/OLATUpgrade_11_4_0.java  | 161 ++++++++
 .../olat/upgrade/_spring/upgradeContext.xml   |   1 +
 src/main/resources/META-INF/persistence.xml   |   2 +-
 .../olat/commons/info/InfoManagerTest.java    |  87 +++++
 .../info/InfoMessageFrontendManagerTest.java  | 344 ++++++++++++++++++
 32 files changed, 1306 insertions(+), 78 deletions(-)
 rename src/main/java/org/olat/{course/nodes/info/SendMailFormatterForCourse.java => commons/info/ui/SendInfoMailFormatter.java} (90%)
 create mode 100644 src/main/java/org/olat/group/ui/run/InfoGroupRunController.java
 create mode 100644 src/main/java/org/olat/group/ui/run/SendGroupMembersMailOption.java
 create mode 100644 src/main/java/org/olat/group/ui/run/_content/run.html
 create mode 100644 src/main/java/org/olat/search/service/indexer/group/GroupInfoIndexer.java
 create mode 100644 src/main/java/org/olat/upgrade/OLATUpgrade_11_4_0.java
 create mode 100644 src/test/java/org/olat/commons/info/InfoMessageFrontendManagerTest.java

diff --git a/src/main/java/org/olat/collaboration/CollaborationTools.java b/src/main/java/org/olat/collaboration/CollaborationTools.java
index 373ca49f731..be000968b50 100644
--- a/src/main/java/org/olat/collaboration/CollaborationTools.java
+++ b/src/main/java/org/olat/collaboration/CollaborationTools.java
@@ -82,6 +82,7 @@ import org.olat.course.ICourse;
 import org.olat.course.run.calendar.CourseLinkProviderController;
 import org.olat.group.BusinessGroup;
 import org.olat.group.BusinessGroupService;
+import org.olat.group.ui.run.InfoGroupRunController;
 import org.olat.instantMessaging.InstantMessagingModule;
 import org.olat.instantMessaging.ui.ChatToolController;
 import org.olat.modules.co.ContactFormController;
@@ -221,6 +222,7 @@ public class CollaborationTools implements Serializable {
 	 * cache for Boolean Objects representing the State
 	 */
 	private final static String KEY_NEWS = "news";
+	private final static String KEY_NEWS_ACCESS = "newsAccess";
 	public final static String KEY_CALENDAR_ACCESS = "cal";
 	public final static String KEY_FOLDER_ACCESS = "folder";
 
@@ -250,6 +252,12 @@ public class CollaborationTools implements Serializable {
 		String news = lookupNews();
 		return new SimpleNewsController(ureq, wControl, news);
 	}
+	
+	public Controller createInfoMessageController(UserRequest ureq, WindowControl wControl) {
+		String canAdmin = getNewsAccessProperty();
+		boolean canAccess = "all".equals(canAdmin);
+		return new InfoGroupRunController(ureq, wControl, ores, canAccess);
+	}
 
 	/**
 	 * TODO: rename to getForumController and save instance?
@@ -787,6 +795,35 @@ public class CollaborationTools implements Serializable {
 		return new CollaborationToolsSettingsController(ureq, wControl, ores);
 	}
 
+	/**
+	 * @return Gets the news access property
+	 */
+	public String getNewsAccessProperty() {
+		NarrowedPropertyManager npm = NarrowedPropertyManager.getInstance(ores);
+		Property property = npm.findProperty(null, null, PROP_CAT_BG_COLLABTOOLS, KEY_NEWS_ACCESS);
+		if (property == null) { // no entry
+			return null;
+		}
+		// read the text value of the existing property
+		String text = property.getStringValue();
+		return text;
+	}
+	
+	/**
+	 * @param Save news access property.
+	 */
+	public void saveNewsAccessProperty(String access) {
+		NarrowedPropertyManager npm = NarrowedPropertyManager.getInstance(ores);
+		Property property = npm.findProperty(null, null, PROP_CAT_BG_COLLABTOOLS, KEY_NEWS_ACCESS);
+		if (property == null) { // create a new one
+			Property nP = npm.createPropertyInstance(null, null, PROP_CAT_BG_COLLABTOOLS, KEY_NEWS_ACCESS, null, null, access, null);
+			npm.saveProperty(nP);
+		} else { // modify the existing one
+			property.setStringValue(access);
+			npm.updateProperty(property);
+		}
+	}
+
 	/**
 	 * @return the news; if there is no news yet: return null;
 	 */
@@ -800,6 +837,14 @@ public class CollaborationTools implements Serializable {
 		String text = property.getTextValue();
 		return text;
 	}
+	
+	public Property lookupNewsDBEntry() {
+		NarrowedPropertyManager npm = NarrowedPropertyManager.getInstance(ores);
+		Property property = npm.findProperty(null, null, PROP_CAT_BG_COLLABTOOLS, KEY_NEWS);
+		if (property == null) { // no entry
+			return null;
+		} else return property;
+	}
 
 	/**
 	 * @param news
diff --git a/src/main/java/org/olat/collaboration/CollaborationToolsSettingsController.java b/src/main/java/org/olat/collaboration/CollaborationToolsSettingsController.java
index bfb17ccd5b2..16785b2389a 100644
--- a/src/main/java/org/olat/collaboration/CollaborationToolsSettingsController.java
+++ b/src/main/java/org/olat/collaboration/CollaborationToolsSettingsController.java
@@ -157,12 +157,12 @@ public class CollaborationToolsSettingsController extends BasicController {
 
 	private void addNewsTool(UserRequest ureq) {
 		CollaborationTools collabTools = CollaborationToolsFactory.getInstance().getOrCreateCollaborationTools(businessGroup);
-		String newsValue = collabTools.lookupNews();
+		String access = collabTools.getNewsAccessProperty();
 		
 		if (newsController != null) {
 			removeAsListenerAndDispose(newsController);
 		}
-		newsController = new NewsFormController(ureq, getWindowControl(), (newsValue == null ? "" : newsValue));
+		newsController = new NewsFormController(ureq, getWindowControl(), (access == null ? "" : access));
 		newsController.setEnabled(!managed);
 		listenTo(newsController);
 		
@@ -244,8 +244,8 @@ public class CollaborationToolsSettingsController extends BasicController {
 			}
 		} else if (source == newsController) {
 			if (event.equals(Event.DONE_EVENT)) {
-				String news = newsController.getNewsValue();
-				collabTools.saveNews(news);
+				String access = newsController.getAccessPropertyValue();
+				collabTools.saveNewsAccessProperty(access);
 			}
 			
 		} else if (source == calendarForm) {	
diff --git a/src/main/java/org/olat/collaboration/NewsFormController.java b/src/main/java/org/olat/collaboration/NewsFormController.java
index 19ea2f48457..c0bb6f254bb 100644
--- a/src/main/java/org/olat/collaboration/NewsFormController.java
+++ b/src/main/java/org/olat/collaboration/NewsFormController.java
@@ -22,13 +22,12 @@ package org.olat.collaboration;
 
 import org.olat.core.gui.UserRequest;
 import org.olat.core.gui.components.form.flexible.FormItemContainer;
-import org.olat.core.gui.components.form.flexible.elements.RichTextElement;
+import org.olat.core.gui.components.form.flexible.elements.SingleSelection;
 import org.olat.core.gui.components.form.flexible.impl.FormBasicController;
 import org.olat.core.gui.components.form.flexible.impl.elements.FormSubmit;
 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;
 
 /**
  * Provides a controller for entering an information text for group members
@@ -40,26 +39,19 @@ import org.olat.core.util.StringHelper;
 public class NewsFormController extends FormBasicController {
 
 	private FormSubmit submit;
-	/**
-	 * The rich text element for the information text.
-	 */
-	private RichTextElement newsInputElement;
-
-	/**
-	 * The information text.
-	 */
-	private String news;
+	private SingleSelection newsAccessEl;
+	private String access;
 
 	/**
 	 * Initializes this controller.
 	 * 
 	 * @param ureq The user request.
 	 * @param wControl The window control.
-	 * @param news The information text.
+	 * @param access The information text.
 	 */
-	public NewsFormController(UserRequest ureq, WindowControl wControl, String news) {
+	public NewsFormController(UserRequest ureq, WindowControl wControl, String access) {
 		super(ureq, wControl);
-		this.news = news;
+		this.access = access;
 		initForm(ureq);
 	}
 
@@ -95,9 +87,26 @@ public class NewsFormController extends FormBasicController {
 	protected void initForm(FormItemContainer formLayout, Controller listener, UserRequest ureq) {
 		setFormTitle("news.content");
 		formLayout.setElementCssClass("o_sel_collaboration_news");
-		newsInputElement = uifactory.addRichTextElementForStringData("news.content", "news.content", this.news, 10, -1, false, null,
-				null, formLayout, ureq.getUserSession(), getWindowControl());
-		newsInputElement.setMandatory(true);
+
+		
+		String[] keys = new String[] { "owner", "all" };
+		String values[] = new String[] {
+				// can use folder's translation keys
+				translate("folder.access.owners"),
+				translate("folder.access.all")
+		};
+		newsAccessEl = uifactory.addRadiosVertical("news.access", "news.access", formLayout, keys, values);
+
+		switch (access) {
+		case "all":
+			newsAccessEl.select(access, true);
+			break;
+		case "owner":
+			newsAccessEl.select(access, true);
+			break;
+		default:
+			newsAccessEl.select("owner", true);
+		}
 
 		// Create submit button
 		submit = uifactory.addFormSubmitButton("submit", formLayout);
@@ -105,18 +114,19 @@ public class NewsFormController extends FormBasicController {
 	}
 	
 	public void setEnabled(boolean enabled) {
-		newsInputElement.setEnabled(enabled);
 		submit.setVisible(enabled);
 	}
 
+
 	/**
-	 * Returns the information text.
-	 * 
-	 * @return The information text.
+	 * Returns the access property value.
+	 *
+	 * @return the access property value
 	 */
-	public String getNewsValue() {
-		return newsInputElement.getValue();
+	public String getAccessPropertyValue() {
+		return newsAccessEl.getSelectedKey();
 	}
+	
 
 	/**
 	 * @see org.olat.core.gui.components.form.flexible.impl.FormBasicController#validateFormLogic(org.olat.core.gui.UserRequest)
@@ -124,19 +134,9 @@ public class NewsFormController extends FormBasicController {
 	@Override
 	protected boolean validateFormLogic(UserRequest ureq) {
 		boolean newsOk = true;
-		// This field is mandatory, so check whether there's something in it.
-		String newsText = newsInputElement.getValue();
-		if (!StringHelper.containsNonWhitespace(newsText) || newsText.length() > 4000) {
-			newsOk = false;
-			if(newsText.length() > 4000) {
-				newsInputElement.setErrorKey("input.toolong", new String[] {"4000"});
-			} else {
-				newsInputElement.setErrorKey("form.legende.mandatory", new String[] {});
-			}
-		} else {
-			newsInputElement.clearError();
+		if (!newsAccessEl.isOneSelected()) {
+			newsOk &= false;
 		}
-
 		return newsOk && super.validateFormLogic(ureq);
 	}
 }
diff --git a/src/main/java/org/olat/collaboration/_i18n/LocalStrings_de.properties b/src/main/java/org/olat/collaboration/_i18n/LocalStrings_de.properties
index da7b8ba75af..6e5aa5d73c2 100644
--- a/src/main/java/org/olat/collaboration/_i18n/LocalStrings_de.properties
+++ b/src/main/java/org/olat/collaboration/_i18n/LocalStrings_de.properties
@@ -17,5 +17,6 @@ folder.access=Ordner Schreibberechtigung
 folder.access.all=Alle Mitglieder
 folder.access.owners=Besitzer bzw. Betreuer
 folder.access.title=Ordner Schreibberechtigung konfigurieren
+news.access=Informationen Schreibberechtigung
 news.content=Information an Mitglieder
 selection=Auswahl
diff --git a/src/main/java/org/olat/collaboration/_i18n/LocalStrings_en.properties b/src/main/java/org/olat/collaboration/_i18n/LocalStrings_en.properties
index 2adaa265c3c..f824cb235b4 100644
--- a/src/main/java/org/olat/collaboration/_i18n/LocalStrings_en.properties
+++ b/src/main/java/org/olat/collaboration/_i18n/LocalStrings_en.properties
@@ -17,5 +17,6 @@ folder.access=Folder write permission
 folder.access.all=All members
 folder.access.owners=Coaches
 folder.access.title=Configure folder write permission
+news.access=News write permission
 news.content=Information for members
 selection=Selection
diff --git a/src/main/java/org/olat/commons/info/manager/InfoMessageFrontendManager.java b/src/main/java/org/olat/commons/info/manager/InfoMessageFrontendManager.java
index 125f4383b58..99fda9dbdff 100644
--- a/src/main/java/org/olat/commons/info/manager/InfoMessageFrontendManager.java
+++ b/src/main/java/org/olat/commons/info/manager/InfoMessageFrontendManager.java
@@ -28,6 +28,7 @@ import org.olat.commons.info.model.InfoMessage;
 import org.olat.core.id.Identity;
 import org.olat.core.id.OLATResourceable;
 import org.olat.core.util.resource.OresHelper;
+import org.olat.group.BusinessGroup;
 
 /**
  * 
@@ -41,7 +42,7 @@ public interface InfoMessageFrontendManager {
 
 	public static  final OLATResourceable oresFrontend = OresHelper.lookupType(InfoMessageFrontendManager.class);
 	
-
+	public static final String businessGroupResSubPath = "";// may not be null
 	
 	public InfoMessage loadInfoMessage(Long key);
 	
@@ -57,8 +58,14 @@ public interface InfoMessageFrontendManager {
 	 */
 	public boolean sendInfoMessage(InfoMessage msgs, MailFormatter mailFormatter, Locale locale, Identity from, List<Identity> tos);
 	
+	public void saveInfoMessage(InfoMessage msg);
+	
 	public void deleteInfoMessage(InfoMessage infoMessage);
 	
+	public void deleteInfoMessagesOfIdentity(BusinessGroup businessGroup, Identity identity);
+	
+	public void removeInfoMessagesAndSubscriptionContext(BusinessGroup group);
+	
 	public List<InfoMessage> loadInfoMessageByResource(OLATResourceable ores, String subPath, String businessPath,
 			Date after, Date before, int firstResult, int maxReturn);
 	
diff --git a/src/main/java/org/olat/commons/info/manager/InfoMessageFrontendManagerImpl.java b/src/main/java/org/olat/commons/info/manager/InfoMessageFrontendManagerImpl.java
index d8b89a66614..57aab19ea57 100644
--- a/src/main/java/org/olat/commons/info/manager/InfoMessageFrontendManagerImpl.java
+++ b/src/main/java/org/olat/commons/info/manager/InfoMessageFrontendManagerImpl.java
@@ -28,9 +28,11 @@ import java.util.Set;
 
 import org.olat.commons.info.model.InfoMessage;
 import org.olat.commons.info.notification.InfoSubscriptionManager;
+import org.olat.core.commons.services.notifications.SubscriptionContext;
 import org.olat.core.id.Identity;
 import org.olat.core.id.OLATResourceable;
-import org.olat.core.manager.BasicManager;
+import org.olat.core.logging.OLog;
+import org.olat.core.logging.Tracing;
 import org.olat.core.util.StringHelper;
 import org.olat.core.util.coordinate.CoordinatorManager;
 import org.olat.core.util.event.MultiUserEvent;
@@ -40,6 +42,7 @@ import org.olat.core.util.mail.MailContext;
 import org.olat.core.util.mail.MailContextImpl;
 import org.olat.core.util.mail.MailManager;
 import org.olat.core.util.mail.MailerResult;
+import org.olat.group.BusinessGroup;
 
 /**
  * 
@@ -49,8 +52,10 @@ import org.olat.core.util.mail.MailerResult;
  * Initial Date:  28 juil. 2010 <br>
  * @author srosse, stephane.rosse@frentix.com, http://www.frentix.com
  */
-public class InfoMessageFrontendManagerImpl extends BasicManager implements InfoMessageFrontendManager {
+public class InfoMessageFrontendManagerImpl implements InfoMessageFrontendManager {
 	
+	private static final OLog log = Tracing.createLoggerFor(InfoMessageFrontendManagerImpl.class); 
+			
 	private MailManager mailManager;
 	private CoordinatorManager coordinatorManager;
 	private InfoMessageManager infoMessageManager;
@@ -97,6 +102,11 @@ public class InfoMessageFrontendManagerImpl extends BasicManager implements Info
 	public InfoMessage createInfoMessage(OLATResourceable ores, String subPath, String businessPath, Identity author) {
 		return infoMessageManager.createInfoMessage(ores, subPath, businessPath, author);
 	}
+	
+	@Override
+	public void saveInfoMessage(InfoMessage infoMessage) {
+		infoMessageManager.saveInfoMessage(infoMessage);
+	}
 
 	@Override
 	public boolean sendInfoMessage(InfoMessage infoMessage, MailFormatter mailFormatter, Locale locale, Identity from, List<Identity> tos) {
@@ -136,7 +146,7 @@ public class InfoMessageFrontendManagerImpl extends BasicManager implements Info
 				MailerResult result = mailManager.sendMessage(bundle);
 				send = result.isSuccessful();
 			} catch (Exception e) {
-				logError("Cannot send info messages", e);
+				log.error("Cannot send info messages", e);
 			}
 		}
 
@@ -151,6 +161,28 @@ public class InfoMessageFrontendManagerImpl extends BasicManager implements Info
 		infoMessageManager.deleteInfoMessage(infoMessage);
 		infoSubscriptionManager.markPublisherNews(infoMessage.getOLATResourceable(), infoMessage.getResSubPath());
 	}
+	
+	@Override
+	public void deleteInfoMessagesOfIdentity(BusinessGroup businessGroup, Identity identity) {
+		List<InfoMessage> infoMessages = infoMessageManager.loadInfoMessagesOfIdentity(businessGroup, identity);
+		for (InfoMessage infoMessage : infoMessages) {
+			infoMessageManager.deleteInfoMessage(infoMessage);
+			infoSubscriptionManager.markPublisherNews(infoMessage.getOLATResourceable(), infoMessage.getResSubPath());
+		}		
+	}
+	
+	@Override
+	public void removeInfoMessagesAndSubscriptionContext(BusinessGroup group) {
+		List<InfoMessage> messages = infoMessageManager.loadInfoMessageByResource(group,
+				InfoMessageFrontendManager.businessGroupResSubPath, null, null, null, 0, 0);
+		for (InfoMessage im : messages) {
+			infoMessageManager.deleteInfoMessage(im);
+		}			
+		String resName = group.getResourceableTypeName();
+		Long resId = group.getResourceableId();
+		SubscriptionContext subscriptionContext =  new SubscriptionContext(resName, resId, "");
+		infoSubscriptionManager.deleteSubscriptionContext(subscriptionContext);
+	}
 
 	@Override
 	public List<InfoMessage> loadInfoMessageByResource(OLATResourceable ores, String subPath, String businessPath,
diff --git a/src/main/java/org/olat/commons/info/manager/InfoMessageManager.java b/src/main/java/org/olat/commons/info/manager/InfoMessageManager.java
index 6d8f1d47d6c..24f4b65916e 100644
--- a/src/main/java/org/olat/commons/info/manager/InfoMessageManager.java
+++ b/src/main/java/org/olat/commons/info/manager/InfoMessageManager.java
@@ -23,9 +23,11 @@ package org.olat.commons.info.manager;
 import java.util.Date;
 import java.util.List;
 
+import org.olat.basesecurity.IdentityRef;
 import org.olat.commons.info.model.InfoMessage;
 import org.olat.core.id.Identity;
 import org.olat.core.id.OLATResourceable;
+import org.olat.group.BusinessGroupRef;
 
 public abstract class InfoMessageManager {
 
@@ -43,6 +45,8 @@ public abstract class InfoMessageManager {
 	
 	public abstract void deleteInfoMessage(InfoMessage infoMessage);
 	
+	public abstract List<InfoMessage> loadInfoMessagesOfIdentity(BusinessGroupRef businessGroup, IdentityRef identity);
+	
 	public abstract InfoMessage loadInfoMessageByKey(Long key);
 	
 	public abstract List<InfoMessage> loadInfoMessageByResource(OLATResourceable ores, String subPath, String businessPath,
diff --git a/src/main/java/org/olat/commons/info/manager/InfoMessageManagerImpl.java b/src/main/java/org/olat/commons/info/manager/InfoMessageManagerImpl.java
index 1aeda700178..eeccc4f8cfe 100644
--- a/src/main/java/org/olat/commons/info/manager/InfoMessageManagerImpl.java
+++ b/src/main/java/org/olat/commons/info/manager/InfoMessageManagerImpl.java
@@ -25,6 +25,7 @@ import java.util.Date;
 import java.util.List;
 import java.util.StringTokenizer;
 
+import org.olat.basesecurity.IdentityRef;
 import org.olat.commons.info.model.InfoMessage;
 import org.olat.commons.info.model.InfoMessageImpl;
 import org.olat.core.commons.persistence.DB;
@@ -32,6 +33,7 @@ import org.olat.core.commons.persistence.DBQuery;
 import org.olat.core.id.Identity;
 import org.olat.core.id.OLATResourceable;
 import org.olat.core.util.StringHelper;
+import org.olat.group.BusinessGroupRef;
 
 /**
  * 
@@ -95,19 +97,43 @@ public class InfoMessageManagerImpl extends InfoMessageManager {
 			}
 		}
 	}
+	
+
+	@Override
+	public List<InfoMessage> loadInfoMessagesOfIdentity(BusinessGroupRef businessGroup, IdentityRef identity) {
+		StringBuilder sb = new StringBuilder();
+		sb.append("select msg from ").append(InfoMessageImpl.class.getName()).append(" msg")
+			.append(" inner join fetch msg.author author")
+			.append(" inner join fetch author.user")
+			.append(" left join fetch msg.modifier modifier")
+			.append(" left join fetch modifier.user")
+			.append(" where author.key=:authorKey")
+			.append(" and msg.resId=:groupKey");
+		return dbInstance.getCurrentEntityManager()
+				.createQuery(sb.toString(), InfoMessage.class)
+				.setParameter("authorKey",identity.getKey())
+				.setParameter("groupKey", businessGroup.getKey())
+				.getResultList();
+	}
 
 	@Override
 	public InfoMessage loadInfoMessageByKey(Long key) {
 		StringBuilder sb = new StringBuilder();
-		sb.append("select msg from ").append(InfoMessageImpl.class.getName())
-			.append(" msg where msg.key=:key");
+		sb.append("select msg from ").append(InfoMessageImpl.class.getName()).append(" msg")
+			.append(" inner join fetch msg.author author")
+			.append(" inner join fetch author.user")
+			.append(" left join fetch msg.modifier modifier")
+			.append(" left join fetch modifier.user")
+			.append(" where msg.key=:key");
 		
-		DBQuery query = dbInstance.createQuery(sb.toString());
-		query.setLong("key", key);
-		@SuppressWarnings("unchecked")
-		List<InfoMessage> msgs = query.list();
-		if(msgs.isEmpty()) return null;
-		return msgs.get(0);
+		List<InfoMessage> infoMessages = dbInstance.getCurrentEntityManager()
+				.createQuery(sb.toString(), InfoMessage.class)
+				.setParameter("key", key)
+				.getResultList();
+		if (infoMessages.size() == 0) {
+			return null;
+		}
+		return infoMessages.get(0);
 	}
 
 	@Override
@@ -148,6 +174,13 @@ public class InfoMessageManagerImpl extends InfoMessageManager {
 		
 		sb.append(" from ").append(InfoMessageImpl.class.getName()).append(" msg");
 		
+		if (!count) {
+			sb.append(" inner join fetch msg.author author")
+			.append(" inner join fetch author.user")
+			.append(" left join fetch msg.modifier modifier")
+			.append(" left join fetch modifier.user");
+		}
+		
 		if(ores != null) {
 			appendAnd(sb, "msg.resId=:resId and msg.resName=:resName ");
 		}
diff --git a/src/main/java/org/olat/commons/info/model/InfoMessageImpl.java b/src/main/java/org/olat/commons/info/model/InfoMessageImpl.java
index b626c565452..5e976126033 100644
--- a/src/main/java/org/olat/commons/info/model/InfoMessageImpl.java
+++ b/src/main/java/org/olat/commons/info/model/InfoMessageImpl.java
@@ -22,31 +22,82 @@ package org.olat.commons.info.model;
 
 import java.util.Date;
 
-import org.olat.core.commons.persistence.PersistentObject;
+import javax.persistence.Column;
+import javax.persistence.Entity;
+import javax.persistence.FetchType;
+import javax.persistence.GeneratedValue;
+import javax.persistence.Id;
+import javax.persistence.JoinColumn;
+import javax.persistence.ManyToOne;
+import javax.persistence.Table;
+import javax.persistence.Temporal;
+import javax.persistence.TemporalType;
+
+import org.hibernate.annotations.GenericGenerator;
+import org.hibernate.annotations.Parameter;
+import org.olat.basesecurity.IdentityImpl;
+import org.olat.core.id.CreateInfo;
 import org.olat.core.id.Identity;
 import org.olat.core.id.OLATResourceable;
+import org.olat.core.id.Persistable;
 
-public class InfoMessageImpl extends PersistentObject implements InfoMessage {
+/**
+ * Initial Date: 17.03.2017
+ * @author fkiefer, fabian.kiefer@frentix.com, www.frentix.com
+ */
+@Entity(name="infomessage")
+@Table(name="o_info_message")
+public class InfoMessageImpl implements InfoMessage, CreateInfo, Persistable {
 
 	private static final long serialVersionUID = 6373476657660866469L;
-
+	
+	@Id
+	@GeneratedValue(generator = "system-uuid")
+	@GenericGenerator(name = "system-uuid", strategy = "enhanced-sequence", parameters={
+		@Parameter(name="sequence_name", value="hibernate_unique_key"),
+		@Parameter(name="force_table_use", value="true"),
+		@Parameter(name="optimizer", value="legacy-hilo"),
+		@Parameter(name="value_column", value="next_hi"),
+		@Parameter(name="increment_size", value="32767"),
+		@Parameter(name="initial_value", value="32767")
+	})
+	@Column(name="info_id", nullable=false, unique=true, insertable=true, updatable=false)
+	private Long key;
+	@Column(name="version", nullable=false, insertable=true, updatable=false)
+	private int version = 0;
+	
+	@Temporal(TemporalType.TIMESTAMP)
+	@Column(name="creationdate", nullable=false, insertable=true, updatable=false)
+	private Date creationDate;
+	@Temporal(TemporalType.TIMESTAMP)
+	@Column(name="modificationdate", nullable=true, insertable=true, updatable=false)
 	private Date modificationDate;
 	
+	@Column(name="title", nullable=true, insertable=true, updatable=true)
 	private String title;
+	@Column(name="message", nullable=true, insertable=true, updatable=true)
 	private String message;
 	
+	@Column(name="resid", nullable=false, insertable=true, updatable=false)
 	private Long resId;
+	@Column(name="resname", nullable=false, insertable=true, updatable=false)
 	private String resName;
-	private String subPath;
+	@Column(name="ressubpath", nullable=true, insertable=true, updatable=false)
+	private String resSubPath;
+	@Column(name="businesspath", nullable=true, insertable=true, updatable=false)
 	private String businessPath;
 	
+	@ManyToOne(targetEntity=IdentityImpl.class, fetch=FetchType.LAZY, optional=false)
+	@JoinColumn(name="fk_author_id", nullable=false, insertable=true, updatable=false)
 	private Identity author;
+	@ManyToOne(targetEntity=IdentityImpl.class, fetch=FetchType.LAZY, optional=true)
+	@JoinColumn(name="fk_modifier_id", nullable=true, insertable=true, updatable=true)
 	private Identity modifier;
 	
 	public InfoMessageImpl() {
 		//
 	}
-
+	
 	public Date getModificationDate() {
 		return modificationDate;
 	}
@@ -54,6 +105,10 @@ public class InfoMessageImpl extends PersistentObject implements InfoMessage {
 	public void setModificationDate(Date modificationDate) {
 		this.modificationDate = modificationDate;
 	}
+	
+	public void setCreationDate(Date creationDate) {
+		this.creationDate= creationDate;
+	}
 
 	public String getTitle() {
 		return title;
@@ -88,11 +143,11 @@ public class InfoMessageImpl extends PersistentObject implements InfoMessage {
 	}
 
 	public String getResSubPath() {
-		return subPath;
+		return resSubPath;
 	}
 
 	public void setResSubPath(String subPath) {
-		this.subPath = subPath;
+		this.resSubPath = subPath;
 	}
 
 	public String getBusinessPath() {
@@ -150,4 +205,20 @@ public class InfoMessageImpl extends PersistentObject implements InfoMessage {
 		}
 		return false;
 	}
+
+	@Override
+	public boolean equalsByPersistableKey(Persistable persistable) {
+		return equals(persistable);
+	}
+
+	@Override
+	public Long getKey() {
+		return key;
+	}
+
+	
+	@Override
+	public Date getCreationDate() {
+		return creationDate;
+	}
 }
diff --git a/src/main/java/org/olat/commons/info/notification/InfoMessageNotificationHandler.java b/src/main/java/org/olat/commons/info/notification/InfoMessageNotificationHandler.java
index 64073fce04c..2c6c8baca41 100644
--- a/src/main/java/org/olat/commons/info/notification/InfoMessageNotificationHandler.java
+++ b/src/main/java/org/olat/commons/info/notification/InfoMessageNotificationHandler.java
@@ -26,6 +26,7 @@ import java.util.Locale;
 
 import org.olat.commons.info.manager.InfoMessageManager;
 import org.olat.commons.info.model.InfoMessage;
+import org.olat.core.CoreSpringFactory;
 import org.olat.core.commons.services.notifications.NotificationHelper;
 import org.olat.core.commons.services.notifications.NotificationsHandler;
 import org.olat.core.commons.services.notifications.NotificationsManager;
@@ -41,6 +42,8 @@ import org.olat.core.id.context.BusinessControlFactory;
 import org.olat.core.logging.LogDelegator;
 import org.olat.core.util.Util;
 import org.olat.core.util.resource.OresHelper;
+import org.olat.group.BusinessGroup;
+import org.olat.group.BusinessGroupService;
 import org.olat.repository.RepositoryEntry;
 import org.olat.repository.RepositoryManager;
 
@@ -72,15 +75,24 @@ public class InfoMessageNotificationHandler extends LogDelegator implements Noti
 				final Long resId = subscriber.getPublisher().getResId();
 				final String resName = subscriber.getPublisher().getResName();
 				String resSubPath = subscriber.getPublisher().getSubidentifier();
-
-				RepositoryEntry re = RepositoryManager.getInstance().lookupRepositoryEntry(OresHelper.createOLATResourceableInstance(resName, resId), false);
-				if(re.getRepositoryEntryStatus().isClosed() || re.getRepositoryEntryStatus().isUnpublished()) {
-					return NotificationsManager.getInstance().getNoSubscriptionInfo();
-				}
 				
-				String displayName = re.getDisplayname();
+				String displayName, notificationtitle;
+				if ("BusinessGroup".equals(resName)) {
+					BusinessGroupService groupService = CoreSpringFactory.getImpl(BusinessGroupService.class);
+					BusinessGroup group = groupService.loadBusinessGroup(resId);
+					displayName = group.getName();
+					notificationtitle = "notification.title.group";
+				} else {
+					RepositoryEntry re = RepositoryManager.getInstance().lookupRepositoryEntry(OresHelper.createOLATResourceableInstance(resName, resId), false);
+					if(re.getRepositoryEntryStatus().isClosed() || re.getRepositoryEntryStatus().isUnpublished()) {
+						return NotificationsManager.getInstance().getNoSubscriptionInfo();
+					}					
+					displayName = re.getDisplayname();	
+					notificationtitle = "notification.title";
+				}				
+
 				Translator translator = Util.createPackageTranslator(this.getClass(), locale);
-				String title = translator.translate("notification.title", new String[]{ displayName });
+				String title = translator.translate(notificationtitle, new String[]{ displayName });
 				si = new SubscriptionInfo(subscriber.getKey(), p.getType(), new TitleItem(title, CSS_CLASS_ICON), null);
 				
 				OLATResourceable ores = OresHelper.createOLATResourceableInstance(resName, resId);
diff --git a/src/main/java/org/olat/commons/info/notification/InfoSubscriptionManager.java b/src/main/java/org/olat/commons/info/notification/InfoSubscriptionManager.java
index 770f44056d2..210b10e8362 100644
--- a/src/main/java/org/olat/commons/info/notification/InfoSubscriptionManager.java
+++ b/src/main/java/org/olat/commons/info/notification/InfoSubscriptionManager.java
@@ -61,4 +61,7 @@ public abstract class InfoSubscriptionManager {
 	public abstract void unsubscribe(OLATResourceable resource, String subPath, Identity identity);
 	
 	public abstract void markPublisherNews(OLATResourceable resource, String subPath);
+
+	public abstract void deleteSubscriptionContext(SubscriptionContext context);
+	
 }
diff --git a/src/main/java/org/olat/commons/info/notification/InfoSubscriptionManagerImpl.java b/src/main/java/org/olat/commons/info/notification/InfoSubscriptionManagerImpl.java
index 3c2e636739d..369d27aeb3b 100644
--- a/src/main/java/org/olat/commons/info/notification/InfoSubscriptionManagerImpl.java
+++ b/src/main/java/org/olat/commons/info/notification/InfoSubscriptionManagerImpl.java
@@ -122,4 +122,9 @@ public class InfoSubscriptionManagerImpl extends InfoSubscriptionManager {
 		SubscriptionContext context = getInfoSubscriptionContext(resource, subPath);
 		notificationsManager.markPublisherNews(context, null, true);
 	}
+	
+	@Override
+	public void deleteSubscriptionContext(SubscriptionContext context) {
+		notificationsManager.delete(context);
+	}
 }
diff --git a/src/main/java/org/olat/commons/info/notification/_i18n/LocalStrings_de.properties b/src/main/java/org/olat/commons/info/notification/_i18n/LocalStrings_de.properties
index 3f14671a2da..f6e338c0fa3 100644
--- a/src/main/java/org/olat/commons/info/notification/_i18n/LocalStrings_de.properties
+++ b/src/main/java/org/olat/commons/info/notification/_i18n/LocalStrings_de.properties
@@ -1,3 +1,4 @@
 #Mon Mar 02 09:54:04 CET 2009
 notification.title=Mitteilungen im Kurs "{0}"
+notification.title.group=Mitteilungen in der Gruppe "{0}"
 notifications.entry=Mitteilung "{0}" eingestellt von {1}
\ No newline at end of file
diff --git a/src/main/java/org/olat/commons/info/notification/_i18n/LocalStrings_en.properties b/src/main/java/org/olat/commons/info/notification/_i18n/LocalStrings_en.properties
index 362d53e0e4f..185e58e7dfa 100644
--- a/src/main/java/org/olat/commons/info/notification/_i18n/LocalStrings_en.properties
+++ b/src/main/java/org/olat/commons/info/notification/_i18n/LocalStrings_en.properties
@@ -1,3 +1,4 @@
 #Fri Sep 30 15:26:44 CEST 2016
 notification.title=Notifications in course "{0}"
+notification.title.group=Notifications in group "{0}"
 notifications.entry=Message "{0}" added by {1}
diff --git a/src/main/java/org/olat/commons/info/ui/InfoDisplayController.java b/src/main/java/org/olat/commons/info/ui/InfoDisplayController.java
index 4de458c208d..16d73dc4c2c 100644
--- a/src/main/java/org/olat/commons/info/ui/InfoDisplayController.java
+++ b/src/main/java/org/olat/commons/info/ui/InfoDisplayController.java
@@ -63,6 +63,7 @@ import org.olat.core.util.coordinate.CoordinatorManager;
 import org.olat.core.util.coordinate.LockResult;
 import org.olat.core.util.resource.OresHelper;
 import org.olat.course.nodes.info.InfoCourseNodeConfiguration;
+import org.olat.group.BusinessGroup;
 import org.olat.modules.ModuleConfiguration;
 import org.olat.user.UserManager;
 import org.olat.util.logging.activity.LoggingResourceable;
@@ -108,6 +109,24 @@ public class InfoDisplayController extends FormBasicController {
 	private MailFormatter sendMailFormatter;
 	private List<SendMailOption> sendMailOptions = new ArrayList<SendMailOption>();
 	
+	public InfoDisplayController(UserRequest ureq, WindowControl wControl, InfoSecurityCallback secCallback,
+			BusinessGroup businessGroup, String resSubPath, String businessPath) {
+		super(ureq, wControl, "display");
+		userManager = CoreSpringFactory.getImpl(UserManager.class);
+		infoMessageManager = CoreSpringFactory.getImpl(InfoMessageFrontendManager.class);
+		this.secCallback = secCallback;
+		this.ores = businessGroup.getResource();
+		this.resSubPath = resSubPath;
+		this.businessPath = businessPath;
+		// default show 10 messages for groups
+		maxResults = maxResultsConfig = 10;
+		
+		initForm(ureq);	
+		
+		// now load with configuration
+		loadMessages();		
+	}
+	
 	public InfoDisplayController(UserRequest ureq, WindowControl wControl, ModuleConfiguration config,
 			InfoSecurityCallback secCallback, OLATResourceable ores, String resSubPath, String businessPath) {
 		super(ureq, wControl, "display");
diff --git a/src/main/java/org/olat/course/nodes/info/SendMailFormatterForCourse.java b/src/main/java/org/olat/commons/info/ui/SendInfoMailFormatter.java
similarity index 90%
rename from src/main/java/org/olat/course/nodes/info/SendMailFormatterForCourse.java
rename to src/main/java/org/olat/commons/info/ui/SendInfoMailFormatter.java
index 41d9b966f1a..edff133c6db 100644
--- a/src/main/java/org/olat/course/nodes/info/SendMailFormatterForCourse.java
+++ b/src/main/java/org/olat/commons/info/ui/SendInfoMailFormatter.java
@@ -19,7 +19,7 @@
  */
 
 
-package org.olat.course.nodes.info;
+package org.olat.commons.info.ui;
 
 import java.text.DateFormat;
 import java.util.List;
@@ -41,20 +41,19 @@ import org.olat.core.id.context.ContextEntry;
  * Initial Date:  24 aug. 2010 <br>
  * @author srosse, stephane.rosse@frentix.com, http://www.frentix.com
  */
-public class SendMailFormatterForCourse implements MailFormatter {
+public class SendInfoMailFormatter implements MailFormatter {
 	
-	private final String courseTitle;
+	private final String title;
 	private final String businessPath;
 	private final Translator translator;
 	
-	public SendMailFormatterForCourse(String courseTitle, String businessPath, Translator translator) {
-		this.courseTitle = courseTitle;
+	public SendInfoMailFormatter(String title, String businessPath, Translator translator) {
+		this.title = title;
 		this.translator = translator;
 		this.businessPath = businessPath;
 	}
 	
 	@Override
-	//fxdiff VCRP-16: intern mail system
 	public String getBusinessPath() {
 		return businessPath;
 	}
@@ -76,7 +75,7 @@ public class SendMailFormatterForCourse implements MailFormatter {
 		
 		StringBuilder sb = new StringBuilder();
 		sb.append("<div style='background: #FAFAFA; border: 1px solid #eee; border-radius: 5px; padding: 0 0.5em 0.5em 0.5em; margin: 1em 0 1em 0;' class='o_m_h'>");		
-		sb.append("<h3>").append(translator.translate("mail.body.title", new String[]{courseTitle})).append("</h3>");
+		sb.append("<h3>").append(translator.translate("mail.body.title", new String[]{title})).append("</h3>");
 		sb.append("<div style='font-size: 90%; color: #888' class='o_m_a'>").append(translator.translate("mail.body.from", new String[]{author, date})).append("</div>");
 		sb.append("</div>");
 		
diff --git a/src/main/java/org/olat/course/nodes/info/InfoRunController.java b/src/main/java/org/olat/course/nodes/info/InfoRunController.java
index e4e21ca0afe..6245d6f1dbe 100644
--- a/src/main/java/org/olat/course/nodes/info/InfoRunController.java
+++ b/src/main/java/org/olat/course/nodes/info/InfoRunController.java
@@ -30,6 +30,7 @@ import org.olat.commons.info.notification.InfoSubscription;
 import org.olat.commons.info.notification.InfoSubscriptionManager;
 import org.olat.commons.info.ui.InfoDisplayController;
 import org.olat.commons.info.ui.InfoSecurityCallback;
+import org.olat.commons.info.ui.SendInfoMailFormatter;
 import org.olat.commons.info.ui.SendSubscriberMailOption;
 import org.olat.core.CoreSpringFactory;
 import org.olat.core.commons.services.notifications.PublisherData;
@@ -136,7 +137,7 @@ public class InfoRunController extends BasicController {
 		infoDisplayController.addSendMailOptions(new SendSubscriberMailOption(infoResourceable, resSubPath, CoreSpringFactory.getImpl(InfoMessageFrontendManager.class)));
 		infoDisplayController.addSendMailOptions(new SendMembersMailOption(course.getCourseEnvironment().getCourseGroupManager().getCourseResource(),
 				RepositoryManager.getInstance(), repositoryService, CoreSpringFactory.getImpl(BusinessGroupService.class)));
-		MailFormatter mailFormatter = new SendMailFormatterForCourse(course.getCourseTitle(), businessPath, getTranslator());
+		MailFormatter mailFormatter = new SendInfoMailFormatter(course.getCourseTitle(), businessPath, getTranslator());
 		infoDisplayController.setSendMailFormatter(mailFormatter);
 		listenTo(infoDisplayController);
 
diff --git a/src/main/java/org/olat/group/manager/BusinessGroupServiceImpl.java b/src/main/java/org/olat/group/manager/BusinessGroupServiceImpl.java
index fd798171b2e..e177c724e0f 100644
--- a/src/main/java/org/olat/group/manager/BusinessGroupServiceImpl.java
+++ b/src/main/java/org/olat/group/manager/BusinessGroupServiceImpl.java
@@ -44,6 +44,7 @@ import org.olat.basesecurity.IdentityRef;
 import org.olat.basesecurity.SecurityGroup;
 import org.olat.collaboration.CollaborationTools;
 import org.olat.collaboration.CollaborationToolsFactory;
+import org.olat.commons.info.manager.InfoMessageFrontendManager;
 import org.olat.core.CoreSpringFactory;
 import org.olat.core.commons.persistence.DB;
 import org.olat.core.commons.services.notifications.NotificationsManager;
@@ -161,6 +162,8 @@ public class BusinessGroupServiceImpl implements BusinessGroupService, UserDataD
 	@Autowired
 	private NotificationsManager notificationsManager;
 	@Autowired
+	private InfoMessageFrontendManager infoMessageManager;
+	@Autowired
 	private MailManager mailManager;
 	@Autowired
 	private ACReservationDAO reservationDao;
@@ -772,9 +775,11 @@ public class BusinessGroupServiceImpl implements BusinessGroupService, UserDataD
 			// 5) delete the publisher attached to this group (e.g. the forum and folder
 			// publisher)
 			notificationsManager.deletePublishersOf(group);
-			// 6) the group
+			// 6) delete info messages and subscription context associated with this group
+			infoMessageManager.removeInfoMessagesAndSubscriptionContext(group);
+			// 7) the group
 			businessGroupDAO.delete(group);
-			// 7) delete the associated security groups
+			// 8) delete the associated security groups
 			//TODO group
 			
 			dbInstance.commit();
@@ -1030,7 +1035,7 @@ public class BusinessGroupServiceImpl implements BusinessGroupService, UserDataD
 
 	private void removeParticipant(Identity ureqIdentity, Identity identity, BusinessGroup group, MailPackage mailing,
 			List<BusinessGroupModifiedEvent.Deferred> events) {
-
+		infoMessageManager.deleteInfoMessagesOfIdentity(group, identity);
 		boolean removed = businessGroupRelationDAO.removeRole(identity, group, GroupRoles.participant.name());
 		if(removed) {
 			// notify currently active users of this business group
diff --git a/src/main/java/org/olat/group/ui/run/BusinessGroupMainRunController.java b/src/main/java/org/olat/group/ui/run/BusinessGroupMainRunController.java
index 56cb92b4e61..b9dc197c116 100644
--- a/src/main/java/org/olat/group/ui/run/BusinessGroupMainRunController.java
+++ b/src/main/java/org/olat/group/ui/run/BusinessGroupMainRunController.java
@@ -745,7 +745,7 @@ public class BusinessGroupMainRunController extends MainLayoutBasicController im
 		addToHistory(ureq, bwControl);
 		
 		CollaborationTools collabTools = CollaborationToolsFactory.getInstance().getOrCreateCollaborationTools(businessGroup);
-		collabToolCtr = collabTools.createNewsController(ureq, bwControl);
+		collabToolCtr = collabTools.createInfoMessageController(ureq, bwControl);
 		listenTo(collabToolCtr);
 		mainPanel.setContent(collabToolCtr.getInitialComponent());
 	}
diff --git a/src/main/java/org/olat/group/ui/run/InfoGroupRunController.java b/src/main/java/org/olat/group/ui/run/InfoGroupRunController.java
new file mode 100644
index 00000000000..aba5469da6e
--- /dev/null
+++ b/src/main/java/org/olat/group/ui/run/InfoGroupRunController.java
@@ -0,0 +1,214 @@
+/**
+ * <a href="http://www.openolat.org">
+ * OpenOLAT - Online Learning and Training</a><br>
+ * <p>
+ * Licensed under the Apache License, Version 2.0 (the "License"); <br>
+ * you may not use this file except in compliance with the License.<br>
+ * You may obtain a copy of the License at the
+ * <a href="http://www.apache.org/licenses/LICENSE-2.0">Apache homepage</a>
+ * <p>
+ * Unless required by applicable law or agreed to in writing,<br>
+ * software distributed under the License is distributed on an "AS IS" BASIS, <br>
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. <br>
+ * See the License for the specific language governing permissions and <br>
+ * limitations under the License.
+ * <p>
+ * Initial code contributed and copyrighted by<br>
+ * frentix GmbH, http://www.frentix.com
+ * <p>
+ */
+package org.olat.group.ui.run;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.StringTokenizer;
+
+import org.olat.commons.info.manager.InfoMessageFrontendManager;
+import org.olat.commons.info.manager.MailFormatter;
+import org.olat.commons.info.notification.InfoSubscription;
+import org.olat.commons.info.notification.InfoSubscriptionManager;
+import org.olat.commons.info.ui.InfoDisplayController;
+import org.olat.commons.info.ui.InfoSecurityCallback;
+import org.olat.commons.info.ui.SendInfoMailFormatter;
+import org.olat.commons.info.ui.SendMailOption;
+import org.olat.commons.info.ui.SendSubscriberMailOption;
+import org.olat.core.commons.services.notifications.PublisherData;
+import org.olat.core.commons.services.notifications.SubscriptionContext;
+import org.olat.core.commons.services.notifications.ui.ContextualSubscriptionController;
+import org.olat.core.gui.UserRequest;
+import org.olat.core.gui.components.Component;
+import org.olat.core.gui.components.velocity.VelocityContainer;
+import org.olat.core.gui.control.Controller;
+import org.olat.core.gui.control.Event;
+import org.olat.core.gui.control.WindowControl;
+import org.olat.core.gui.control.controller.BasicController;
+import org.olat.core.id.OLATResourceable;
+import org.olat.core.id.Roles;
+import org.olat.core.util.UserSession;
+import org.olat.core.util.resource.OresHelper;
+import org.olat.group.BusinessGroup;
+import org.olat.group.BusinessGroupService;
+import org.springframework.beans.factory.annotation.Autowired;
+
+/**
+ * Initial Date: 15.03.2017
+ * @author fkiefer, fabian.kiefer@frentix.com, www.frentix.com
+ */
+public class InfoGroupRunController extends BasicController {
+	
+	public static final String resSubPath = InfoMessageFrontendManager.businessGroupResSubPath;
+
+	private final VelocityContainer runVC;
+	private final InfoDisplayController infoDisplayController;
+	private ContextualSubscriptionController subscriptionController;
+	
+	private final String businessPath;
+	private InfoSubscriptionManager subscriptionManager;
+	
+	@Autowired
+	private	BusinessGroupService groupService;
+	@Autowired
+	private InfoMessageFrontendManager messageManager;
+
+
+	public InfoGroupRunController(UserRequest ureq, WindowControl wControl, BusinessGroup businessGroup, boolean canAccess) {
+		super(ureq, wControl);
+		
+		long groupId = businessGroup.getKey();
+		OLATResourceable infoResourceable = new InfoOLATGroupResourceable(groupId);
+		businessPath = normalizeBusinessPath(wControl.getBusinessControl().getAsString());
+		
+		UserSession usess = ureq.getUserSession();
+		if(!usess.getRoles().isGuestOnly()) {
+			subscriptionManager = InfoSubscriptionManager.getInstance();
+			SubscriptionContext subContext = subscriptionManager.getInfoSubscriptionContext(infoResourceable, resSubPath);
+			PublisherData pdata = subscriptionManager.getInfoPublisherData(infoResourceable, businessPath);
+			subscriptionController = new ContextualSubscriptionController(ureq, getWindowControl(), subContext, pdata);
+			listenTo(subscriptionController);
+		}
+				
+		Roles roles = usess.getRoles();
+		boolean canAdmin = roles.isOLATAdmin() || roles.isGroupManager();
+		boolean canAdd = canAdmin || canAccess;
+
+		InfoSecurityCallback secCallback = new InfoGroupSecurityCallback(canAdd, canAdmin);
+		infoDisplayController = new InfoDisplayController(ureq, wControl, secCallback, businessGroup, resSubPath, businessPath);
+		SendMailOption subscribers = new SendSubscriberMailOption(infoResourceable, resSubPath, messageManager);
+		infoDisplayController.addSendMailOptions(subscribers);
+		SendMailOption groupMembers = new SendGroupMembersMailOption(groupService, businessGroup);
+		infoDisplayController.addSendMailOptions(groupMembers);
+		MailFormatter mailFormatter = new SendInfoMailFormatter(businessGroup.getName(), businessPath, getTranslator());
+		infoDisplayController.setSendMailFormatter(mailFormatter);
+		listenTo(infoDisplayController);
+
+		runVC = createVelocityContainer("run");
+		if(subscriptionController != null) {
+			runVC.put("infoSubscription", subscriptionController.getInitialComponent());
+		}
+		runVC.put("displayInfos", infoDisplayController.getInitialComponent());
+		
+		putInitialPanel(runVC);
+	}
+
+	/**
+	 * Remove ROOT, remove identity context entry or duplicate, 
+	 * @param url
+	 * @return
+	 */
+	private String normalizeBusinessPath(String url) {
+		if (url == null) return null;
+		if (url.startsWith("ROOT")) {
+			url = url.substring(4, url.length());
+		}
+		List<String> tokens = new ArrayList<String>();
+		for(StringTokenizer tokenizer = new StringTokenizer(url, "[]"); tokenizer.hasMoreTokens(); ) {
+			String token = tokenizer.nextToken();
+			if(token.startsWith("Identity")) {
+				//The portlet "My courses" add an Identity context entry to the business path
+				//ignore it
+				continue;
+			}
+			if(!tokens.contains(token)) {
+				tokens.add(token);
+			}
+		}
+		
+		StringBuilder sb = new StringBuilder();
+		for(String token:tokens) {
+			sb.append('[').append(token).append(']');
+		}
+		return sb.toString();
+	}
+
+	@Override
+	protected void doDispose() {
+		//
+	}
+
+	@Override
+	protected void event(UserRequest ureq, Component source, Event event) {
+		//
+	}
+	
+	@Override
+	protected void event(UserRequest ureq, Controller source, Event event) {
+		if(source == subscriptionController) {
+			InfoSubscription infoSubscription = subscriptionManager.getInfoSubscription(ureq.getUserSession().getGuiPreferences());
+			if(subscriptionController.isSubscribed()) {
+				infoSubscription.subscribed(businessPath, true);
+			} else {
+				infoSubscription.unsubscribed(businessPath);
+			}
+		}
+		super.event(ureq, source, event);
+	}
+	
+	private class InfoGroupSecurityCallback implements InfoSecurityCallback {
+		private final boolean canAdd;
+		private final boolean canAdmin;
+		
+		public InfoGroupSecurityCallback(boolean canAdd, boolean canAdmin) {
+			this.canAdd = canAdd;
+			this.canAdmin = canAdmin;
+		}
+		
+		@Override
+		public boolean canRead() {
+			return true;
+		}
+
+		@Override
+		public boolean canAdd() {
+			return canAdd;
+		}
+
+		@Override
+		public boolean canEdit() {
+			return canAdd;
+		}
+
+		@Override
+		public boolean canDelete() {
+			return canAdmin;
+		}
+	}
+	
+	private class InfoOLATGroupResourceable implements OLATResourceable {
+		private final Long resId;
+		
+		public InfoOLATGroupResourceable(Long groupId) {
+			this.resId = groupId;
+		}
+		
+		@Override
+		public String getResourceableTypeName() {
+			return OresHelper.calculateTypeName(BusinessGroup.class);
+		}
+
+		@Override
+		public Long getResourceableId() {
+			return resId;
+		}
+	}
+
+}
diff --git a/src/main/java/org/olat/group/ui/run/SendGroupMembersMailOption.java b/src/main/java/org/olat/group/ui/run/SendGroupMembersMailOption.java
new file mode 100644
index 00000000000..aad0902a4d7
--- /dev/null
+++ b/src/main/java/org/olat/group/ui/run/SendGroupMembersMailOption.java
@@ -0,0 +1,64 @@
+/**
+ * <a href="http://www.openolat.org">
+ * OpenOLAT - Online Learning and Training</a><br>
+ * <p>
+ * Licensed under the Apache License, Version 2.0 (the "License"); <br>
+ * you may not use this file except in compliance with the License.<br>
+ * You may obtain a copy of the License at the
+ * <a href="http://www.apache.org/licenses/LICENSE-2.0">Apache homepage</a>
+ * <p>
+ * Unless required by applicable law or agreed to in writing,<br>
+ * software distributed under the License is distributed on an "AS IS" BASIS, <br>
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. <br>
+ * See the License for the specific language governing permissions and <br>
+ * limitations under the License.
+ * <p>
+ * Initial code contributed and copyrighted by<br>
+ * frentix GmbH, http://www.frentix.com
+ * <p>
+ */
+package org.olat.group.ui.run;
+
+import java.util.List;
+import java.util.Locale;
+
+import org.olat.commons.info.ui.SendMailOption;
+import org.olat.core.gui.translator.Translator;
+import org.olat.core.id.Identity;
+import org.olat.core.util.Util;
+import org.olat.group.BusinessGroup;
+import org.olat.group.BusinessGroupService;
+
+/**
+ * Initial Date: 15.03.2017
+ * @author fkiefer, fabian.kiefer@frentix.com, www.frentix.com
+ */
+public class SendGroupMembersMailOption implements SendMailOption {
+
+	private BusinessGroupService groupService;
+	private BusinessGroup businessGroup;
+	
+	
+	public SendGroupMembersMailOption(BusinessGroupService groupService, BusinessGroup businessGroup) {
+		this.groupService = groupService;
+		this.businessGroup = businessGroup;
+	}
+
+	@Override
+	public String getOptionKey() {
+		return "send-mail-group-members";
+	}
+
+	@Override
+	public String getOptionTranslatedName(Locale locale) {
+		Translator translator = Util.createPackageTranslator(SendGroupMembersMailOption.class, locale);
+		return translator.translate("wizard.step1.send_option.member");
+	}
+
+	@Override
+	public List<Identity> getSelectedIdentities() {
+		List<Identity> groupMembers = groupService.getMembers(businessGroup);
+		return groupMembers;
+	}
+
+}
diff --git a/src/main/java/org/olat/group/ui/run/_content/run.html b/src/main/java/org/olat/group/ui/run/_content/run.html
new file mode 100644
index 00000000000..8138c3af743
--- /dev/null
+++ b/src/main/java/org/olat/group/ui/run/_content/run.html
@@ -0,0 +1,6 @@
+#if($r.available("infoSubscription"))
+	<div class="clearfix">
+		$r.render("infoSubscription")
+	</div>
+#end
+$r.render("displayInfos")
diff --git a/src/main/java/org/olat/group/ui/run/_i18n/LocalStrings_de.properties b/src/main/java/org/olat/group/ui/run/_i18n/LocalStrings_de.properties
index dbc2974356e..7331919d4f6 100644
--- a/src/main/java/org/olat/group/ui/run/_i18n/LocalStrings_de.properties
+++ b/src/main/java/org/olat/group/ui/run/_i18n/LocalStrings_de.properties
@@ -11,6 +11,9 @@ grouprun.details.name=Name der Gruppe
 grouprun.details.title=Informationen
 grouprun.disabled=Zurzeit sind alle kollaborativen Werkzeuge in OLAT f\u00FCr Sie gesperrt (z.B. da Sie einen Test bearbeiten). Sie m\u00FCssen den Test zuerst abbrechen / beenden, um kollaborative Werkzeuge benutzen zu k\u00F6nnen.
 grouprun.removedfromgroup=Die Konfiguration dieser Gruppe wurde ver\u00E4ndert (Gruppe gel\u00F6scht, Gruppenmitgliedern ge\u00E4ndert). Schliessen Sie den Tab.
+mail.body.title=Mitteilung aus Kurs {0}
+mail.body.from=Verfasst von {0} am {1}
+mail.body.more=Weitere Mitteilungen
 menutree.administration=Administration
 menutree.administration.alt=Administration
 menutree.calendar=Kalender
@@ -72,3 +75,4 @@ userlist.show.no.waitinglist.title=Warteliste
 userlist.show.no.owners.text=nicht sichtbar
 userlist.show.no.participants.text=nicht sichtbar 
 userlist.show.no.waitinglist.text=nicht sichtbar
+wizard.step1.send_option.member=Gruppenmitglieder und Gruppenbetreuer
diff --git a/src/main/java/org/olat/group/ui/run/_i18n/LocalStrings_en.properties b/src/main/java/org/olat/group/ui/run/_i18n/LocalStrings_en.properties
index 4560447f50f..bb38a432319 100644
--- a/src/main/java/org/olat/group/ui/run/_i18n/LocalStrings_en.properties
+++ b/src/main/java/org/olat/group/ui/run/_i18n/LocalStrings_en.properties
@@ -11,6 +11,9 @@ grouprun.details.name=Group name
 grouprun.details.title=Information
 grouprun.disabled=Currently all collaborative elements in OLAT are locked (e.g. because you are editing a test). Cancel or complete first in order to be able to use collaborative tools.
 grouprun.removedfromgroup=This group's configuration has been modified (group deleted, members changed). Please close the tab. 
+mail.body.title=Notification regarding course {0}
+mail.body.from=Written by {0} on {1}
+mail.body.more=Further information
 menutree.ac=Bookings
 menutree.ac.alt=Bookings
 menutree.administration=Administration
@@ -72,3 +75,4 @@ userlist.show.no.participants.title=Participant
 userlist.show.no.waitinglist.text=not visible
 userlist.show.no.waitinglist.title=Waiting list
 userlist.title=Group members
+wizard.step1.send_option.member=Group members and group coaches
\ No newline at end of file
diff --git a/src/main/java/org/olat/search/_spring/searchContext.xml b/src/main/java/org/olat/search/_spring/searchContext.xml
index 14736634f9f..dd7492657b5 100644
--- a/src/main/java/org/olat/search/_spring/searchContext.xml
+++ b/src/main/java/org/olat/search/_spring/searchContext.xml
@@ -206,6 +206,7 @@
 				<ref bean="groupFolderIndexer" />
 				<ref bean="groupWikiIndexer"	/>
 				<ref bean="groupPortfolioIndexer" />
+				<ref bean="groupInfoIndexer" />
 			</list>
 		</property>
 	</bean>
@@ -215,6 +216,9 @@
 		<property name="collaborationManager" ref="collaborationManager"/>
 	</bean>
 	<bean id="groupWikiIndexer" class="org.olat.search.service.indexer.group.GroupWikiIndexer" />
+	<bean id="groupInfoIndexer" class="org.olat.search.service.indexer.group.GroupInfoIndexer">
+		<property name="infoMessageManager" ref="infoMessageManager"/>
+	</bean>	
 
 	<!-- Portfolio indexers -->
 	<bean id="epDefaultMapIndexer" class="org.olat.search.service.indexer.PortfolioMapIndexer">
diff --git a/src/main/java/org/olat/search/service/indexer/group/GroupInfoIndexer.java b/src/main/java/org/olat/search/service/indexer/group/GroupInfoIndexer.java
new file mode 100644
index 00000000000..85383e3020e
--- /dev/null
+++ b/src/main/java/org/olat/search/service/indexer/group/GroupInfoIndexer.java
@@ -0,0 +1,99 @@
+/**
+ * <a href="http://www.openolat.org">
+ * OpenOLAT - Online Learning and Training</a><br>
+ * <p>
+ * Licensed under the Apache License, Version 2.0 (the "License"); <br>
+ * you may not use this file except in compliance with the License.<br>
+ * You may obtain a copy of the License at the
+ * <a href="http://www.apache.org/licenses/LICENSE-2.0">Apache homepage</a>
+ * <p>
+ * Unless required by applicable law or agreed to in writing,<br>
+ * software distributed under the License is distributed on an "AS IS" BASIS, <br>
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. <br>
+ * See the License for the specific language governing permissions and <br>
+ * limitations under the License.
+ * <p>
+ * Initial code contributed and copyrighted by<br>
+ * frentix GmbH, http://www.frentix.com
+ * <p>
+ */
+package org.olat.search.service.indexer.group;
+
+import java.io.IOException;
+import java.util.List;
+
+import org.apache.lucene.document.Document;
+import org.olat.commons.info.manager.InfoMessageFrontendManager;
+import org.olat.commons.info.manager.InfoMessageManager;
+import org.olat.commons.info.model.InfoMessage;
+import org.olat.core.id.OLATResourceable;
+import org.olat.core.logging.AssertException;
+import org.olat.core.logging.OLog;
+import org.olat.core.logging.Tracing;
+import org.olat.core.util.resource.OresHelper;
+import org.olat.group.BusinessGroup;
+import org.olat.group.ui.run.BusinessGroupMainRunController;
+import org.olat.search.service.SearchResourceContext;
+import org.olat.search.service.document.InfoMessageDocument;
+import org.olat.search.service.indexer.AbstractHierarchicalIndexer;
+import org.olat.search.service.indexer.OlatFullIndexer;
+
+/**
+ * Initial Date: 20.03.2017
+ * @author fkiefer, fabian.kiefer@frentix.com, www.frentix.com
+ */
+public class GroupInfoIndexer extends AbstractHierarchicalIndexer{
+	
+	private static final OLog log = Tracing.createLoggerFor(GroupInfoIndexer.class);
+
+	public static final String TYPE = "type.group.info.message";
+	
+	private final static String SUPPORTED_TYPE_NAME = "org.olat.group.BusinessGroup";
+	
+	private InfoMessageManager infoMessageManager;
+
+	/**
+	 * [used by Spring]
+	 * @param infoMessageManager
+	 */
+	public void setInfoMessageManager(InfoMessageManager infoMessageManager) {
+		this.infoMessageManager = infoMessageManager;
+	}
+	
+	@Override
+	public String getSupportedTypeName() {
+		return SUPPORTED_TYPE_NAME;
+	}
+
+
+	@Override
+	public void doIndex(SearchResourceContext searchResourceContext, Object businessObject, OlatFullIndexer indexerWriter)
+			throws IOException, InterruptedException {
+		if (!(businessObject instanceof BusinessGroup)) throw new AssertException("businessObject must be BusinessGroup");
+		BusinessGroup businessGroup = (BusinessGroup) businessObject;
+		try {
+			SearchResourceContext messagesGroupResourceContext = new SearchResourceContext(searchResourceContext);
+		    messagesGroupResourceContext.setBusinessControlFor(BusinessGroupMainRunController.ORES_TOOLMSG);
+		    messagesGroupResourceContext.setDocumentType(TYPE);
+			doIndexInfos(messagesGroupResourceContext, businessGroup, indexerWriter);
+		} catch (Exception ex) {
+			log.error("Exception indexing businessGroup=" + businessGroup, ex);
+		} catch (Error err) {
+			log.error("Error indexing businessGroup=" + businessGroup, err);
+		}
+	}
+	
+	private void doIndexInfos(SearchResourceContext parentResourceContext, BusinessGroup businessGroup, OlatFullIndexer indexWriter)
+	throws IOException, InterruptedException {
+		List<InfoMessage> messages = infoMessageManager.loadInfoMessageByResource(businessGroup,
+				InfoMessageFrontendManager.businessGroupResSubPath, null, null, null, 0, -1);
+		for(InfoMessage message : messages) {
+			SearchResourceContext searchResourceContext = new SearchResourceContext(parentResourceContext);
+			OLATResourceable ores = OresHelper.createOLATResourceableInstance(InfoMessage.class, message.getKey());
+			searchResourceContext.setBusinessControlFor(ores);
+			Document document = InfoMessageDocument.createDocument(searchResourceContext, message);
+		  indexWriter.addDocument(document);
+		}
+	}
+	
+}
diff --git a/src/main/java/org/olat/upgrade/OLATUpgrade_11_4_0.java b/src/main/java/org/olat/upgrade/OLATUpgrade_11_4_0.java
new file mode 100644
index 00000000000..b53359fe722
--- /dev/null
+++ b/src/main/java/org/olat/upgrade/OLATUpgrade_11_4_0.java
@@ -0,0 +1,161 @@
+/**
+ * <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.util.List;
+
+import org.olat.basesecurity.GroupRoles;
+import org.olat.collaboration.CollaborationTools;
+import org.olat.collaboration.CollaborationToolsFactory;
+import org.olat.commons.info.manager.InfoMessageFrontendManager;
+import org.olat.commons.info.model.InfoMessageImpl;
+import org.olat.core.commons.persistence.DB;
+import org.olat.core.gui.translator.Translator;
+import org.olat.core.id.Identity;
+import org.olat.core.util.Util;
+import org.olat.core.util.i18n.I18nModule;
+import org.olat.group.BusinessGroup;
+import org.olat.group.BusinessGroupService;
+import org.olat.properties.Property;
+import org.olat.repository.RepositoryDeletionModule;
+import org.springframework.beans.factory.annotation.Autowired;
+
+/**
+ * Initial Date: 16.03.2017
+ * @author fkiefer, fabian.kiefer@frentix.com, www.frentix.com
+ */
+public class OLATUpgrade_11_4_0 extends OLATUpgrade {
+	
+	private static final String VERSION = "OLAT_11.4.0";
+	private static final String GROUP_INFO_MSG = "GROUP INFO MESSAGE";
+	
+	@Autowired
+	private DB dbInstance;
+	@Autowired
+	private	BusinessGroupService groupService;
+	@Autowired
+	private CollaborationToolsFactory toolsF;
+	@Autowired
+	private InfoMessageFrontendManager infoMessageManager;
+	@Autowired
+	private RepositoryDeletionModule deletionManager;
+
+	
+	public OLATUpgrade_11_4_0() {
+		super();
+	}
+
+	@Override
+	public String getVersion() {
+		return VERSION;
+	}
+
+	@Override
+	public boolean doPreSystemInitUpgrade(UpgradeManager upgradeManager) {
+		return false;
+	}
+
+	@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 &= upgradeGroupInfoMessage(upgradeManager, uhd);
+		
+		uhd.setInstallationComplete(allOk);
+		upgradeManager.setUpgradesHistory(uhd, VERSION);
+		if(allOk) {
+			log.audit("Finished OLATUpgrade_11_4_0 successfully!");
+		} else {
+			log.audit("OLATUpgrade_11_4_0 not finished, try to restart OpenOLAT!");
+		}
+		return allOk;
+	}
+	
+	private boolean upgradeGroupInfoMessage(UpgradeManager upgradeManager, UpgradeHistoryData uhd) {
+		boolean allOk = true;
+		if (!uhd.getBooleanDataValue(GROUP_INFO_MSG)) {
+			
+			List<BusinessGroup> allBusinessGroups = groupService.loadAllBusinessGroups();
+			for (BusinessGroup businessGroup : allBusinessGroups) {
+				if(businessGroup == null) continue;
+
+				allOk &= processInfoMessage(businessGroup);
+				dbInstance.commitAndCloseSession();
+			}
+			
+			uhd.setBooleanDataValue(GROUP_INFO_MSG, allOk);
+			upgradeManager.setUpgradesHistory(uhd, VERSION);
+		}
+		return allOk;
+	}
+
+	/**
+	 * @param business group
+	 * @return true if upgrade went well
+	 */
+	private boolean processInfoMessage(BusinessGroup businessGroup) {
+		// iterate all groups and translate their singular info message to the new standard
+		try {
+			String businessPath = "[BusinessGroup:" + businessGroup.getKey() + "][toolmsg:0]";
+			int messageCount = infoMessageManager.countInfoMessageByResource(businessGroup.getResource(),
+					InfoMessageFrontendManager.businessGroupResSubPath, businessPath, null, null);
+			// only upgrade if business group has not any info messages of the new kind yet
+			if (1 > messageCount) {
+				CollaborationTools collabTools = toolsF.getOrCreateCollaborationTools(businessGroup);
+				Property property = collabTools.lookupNewsDBEntry();
+				if (property != null) {
+					String oldNews = property.getTextValue();//collabTools.lookupNews();			
+					Identity author;
+					List<Identity> members = groupService.getMembers(businessGroup, GroupRoles.owner.name(), GroupRoles.coach.name());
+					if (members == null || (members != null && members.isEmpty())) {
+						author = deletionManager.getAdminUserIdentity();
+					} else {
+						author = members.get(0);
+					}
+					InfoMessageImpl infoMessage = (InfoMessageImpl)infoMessageManager.createInfoMessage(businessGroup.getResource(),
+							InfoMessageFrontendManager.businessGroupResSubPath, businessPath, author);
+					Translator trans = Util.createPackageTranslator(CollaborationTools.class, I18nModule.getDefaultLocale());
+					String title = trans.translate("news.content");
+					infoMessage.setTitle(title);
+					infoMessage.setMessage(oldNews);
+					infoMessage.setCreationDate(property.getCreationDate());
+					infoMessageManager.saveInfoMessage(infoMessage);
+				} else {
+					log.warn("The group " + businessGroup.getName() + " does not have an info message");
+				}
+			}
+			return true;
+		} catch (Exception e) {
+			log.warn("Update InfoMessage for " + businessGroup.getName() + " failed", e);
+			return false;	
+		}
+	}
+	
+	
+	
+
+}
diff --git a/src/main/java/org/olat/upgrade/_spring/upgradeContext.xml b/src/main/java/org/olat/upgrade/_spring/upgradeContext.xml
index 54ba2c9a97d..64aa7c1335a 100644
--- a/src/main/java/org/olat/upgrade/_spring/upgradeContext.xml
+++ b/src/main/java/org/olat/upgrade/_spring/upgradeContext.xml
@@ -50,6 +50,7 @@
 				<bean id="upgrade_11_0_6" class="org.olat.upgrade.OLATUpgrade_11_0_6"/>
 				<bean id="upgrade_11_2_1" class="org.olat.upgrade.OLATUpgrade_11_2_1"/>
 				<bean id="upgrade_11_3_0" class="org.olat.upgrade.OLATUpgrade_11_3_0"/>
+				<bean id="upgrade_11_4_0" class="org.olat.upgrade.OLATUpgrade_11_4_0"/>				
 			</list>
 		</property>
 	</bean>
diff --git a/src/main/resources/META-INF/persistence.xml b/src/main/resources/META-INF/persistence.xml
index a8d4cc3605a..af690253fee 100644
--- a/src/main/resources/META-INF/persistence.xml
+++ b/src/main/resources/META-INF/persistence.xml
@@ -15,7 +15,6 @@
 		<mapping-file>org/olat/note/NoteImpl.hbm.xml</mapping-file>
 		<mapping-file>org/olat/commons/lifecycle/LifeCycleEntry.hbm.xml</mapping-file>
 		<mapping-file>org/olat/commons/coordinate/cluster/lock/LockImpl.hbm.xml</mapping-file>
-		<mapping-file>org/olat/commons/info/model/InfoMessageImpl.hbm.xml</mapping-file>
 		<mapping-file>org/olat/group/area/BGAreaImpl.hbm.xml</mapping-file>
 		<mapping-file>org/olat/group/area/BGtoAreaRelationImpl.hbm.xml</mapping-file>
 		<mapping-file>org/olat/resource/OLATResourceImpl.hbm.xml</mapping-file>
@@ -84,6 +83,7 @@
 		<class>org.olat.commons.calendar.model.ImportedCalendar</class>
 		<class>org.olat.commons.calendar.model.ImportedToCalendar</class>
 		<class>org.olat.commons.calendar.model.CalendarUserConfiguration</class>
+		<class>org.olat.commons.info.model.InfoMessageImpl</class>
 		<class>org.olat.core.commons.services.lock.pessimistic.PLockImpl</class>
 		<class>org.olat.core.commons.services.notifications.model.SubscriberImpl</class>
 		<class>org.olat.core.commons.services.notifications.model.PublisherImpl</class>
diff --git a/src/test/java/org/olat/commons/info/InfoManagerTest.java b/src/test/java/org/olat/commons/info/InfoManagerTest.java
index 19027ca7a92..d57d2cb0115 100644
--- a/src/test/java/org/olat/commons/info/InfoManagerTest.java
+++ b/src/test/java/org/olat/commons/info/InfoManagerTest.java
@@ -30,6 +30,7 @@ import java.util.Date;
 import java.util.List;
 import java.util.UUID;
 
+import org.junit.Assert;
 import org.junit.Before;
 import org.junit.Test;
 import org.olat.commons.info.manager.InfoMessageManager;
@@ -37,6 +38,9 @@ import org.olat.commons.info.model.InfoMessage;
 import org.olat.core.commons.persistence.DB;
 import org.olat.core.id.Identity;
 import org.olat.core.id.OLATResourceable;
+import org.olat.group.BusinessGroup;
+import org.olat.group.BusinessGroupService;
+import org.olat.repository.RepositoryEntry;
 import org.olat.test.JunitTestHelper;
 import org.olat.test.OlatTestCase;
 import org.springframework.beans.factory.annotation.Autowired;
@@ -61,6 +65,8 @@ public class InfoManagerTest extends OlatTestCase {
 	
 	@Autowired
 	private InfoMessageManager infoMessageManager;
+	@Autowired
+	private BusinessGroupService groupService;
 
 	/**
 	 * Set up a course with learn group and group area
@@ -121,6 +127,87 @@ public class InfoManagerTest extends OlatTestCase {
 		assertEquals(msg.getKey(), retrievedMsg.get(0).getKey());
 	}
 	
+	@Test
+	public void loadInfoMessagesOfIdentity() {
+		Identity id5 = JunitTestHelper.createAndPersistIdentityAsRndUser("info-1");
+		Identity id2 = JunitTestHelper.createAndPersistIdentityAsRndUser("info-2");
+		Identity id3 = JunitTestHelper.createAndPersistIdentityAsRndUser("info-3");
+		Identity id4 = JunitTestHelper.createAndPersistIdentityAsRndUser("info-4");
+		RepositoryEntry resource1 =  JunitTestHelper.createAndPersistRepositoryEntry();
+		BusinessGroup group1 = groupService.createBusinessGroup(null, "gdao1", "gdao1-desc", -1, -1, false, false, resource1);
+		final OLATResourceable ores1 = new OLATResourceable() {
+			@Override
+			public String getResourceableTypeName() {
+				return group1.getResourceableTypeName();
+			}
+			@Override
+			public Long getResourceableId() {
+				return group1.getResourceableId();
+			}			
+		};
+		RepositoryEntry resource2 =  JunitTestHelper.createAndPersistRepositoryEntry();
+		BusinessGroup group2 = groupService.createBusinessGroup(null, "gdao2", "gdao2-desc", -1, -1, false, false, resource2);
+		final OLATResourceable ores2 = new OLATResourceable() {
+			@Override
+			public String getResourceableTypeName() {
+				return group2.getResourceableTypeName();
+			}
+			@Override
+			public Long getResourceableId() {
+				return group2.getResourceableId();
+			}			
+		};
+
+		InfoMessage msg1 = infoMessageManager.createInfoMessage(ores2, null, null, id5);
+		msg1.setTitle("title-1");
+		msg1.setMessage("message-1");
+		assertNotNull(msg1);			
+		infoMessageManager.saveInfoMessage(msg1);
+		
+		InfoMessage msg2 = infoMessageManager.createInfoMessage(ores2, null, null, id2);
+		msg2.setTitle("title-1");
+		msg2.setMessage("message-1");
+		assertNotNull(msg2);			
+		infoMessageManager.saveInfoMessage(msg2);
+		
+		InfoMessage msg3 = infoMessageManager.createInfoMessage(ores1, null, null, id3);
+		msg3.setTitle("title-1");
+		msg3.setMessage("message-1");
+		assertNotNull(msg3);			
+		infoMessageManager.saveInfoMessage(msg3);
+		
+		InfoMessage msg4 = infoMessageManager.createInfoMessage(ores1, null, null, id5);
+		msg4.setTitle("title-1");
+		msg4.setMessage("message-1");
+		assertNotNull(msg4);			
+		infoMessageManager.saveInfoMessage(msg4);
+		
+		InfoMessage msg5 = infoMessageManager.createInfoMessage(ores2, null, null, id5);
+		msg5.setTitle("title-1");
+		msg5.setMessage("message-1");
+		assertNotNull(msg5);			
+		infoMessageManager.saveInfoMessage(msg5);
+		
+		InfoMessage msg6 = infoMessageManager.createInfoMessage(ores2, null, null, id4);
+		msg6.setTitle("title-1");
+		msg6.setMessage("message-1");
+		assertNotNull(msg6);			
+		infoMessageManager.saveInfoMessage(msg6);
+		
+		List<InfoMessage> infoMessages = infoMessageManager.loadInfoMessagesOfIdentity(group2, id5);
+		Assert.assertNotNull(infoMessages);
+		dbInstance.commitAndCloseSession();
+		
+		Assert.assertEquals(2, infoMessages.size());
+		Assert.assertTrue(infoMessages.contains(msg1));
+		Assert.assertFalse(infoMessages.contains(msg2));
+		Assert.assertFalse(infoMessages.contains(msg3));
+		Assert.assertFalse(infoMessages.contains(msg4));
+		Assert.assertTrue(infoMessages.contains(msg5));
+		Assert.assertFalse(infoMessages.contains(msg6));
+	}
+	
+	
 	@Test
 	public void testLoadByResource2() {
 		final String resName = UUID.randomUUID().toString();
diff --git a/src/test/java/org/olat/commons/info/InfoMessageFrontendManagerTest.java b/src/test/java/org/olat/commons/info/InfoMessageFrontendManagerTest.java
new file mode 100644
index 00000000000..7c68049f898
--- /dev/null
+++ b/src/test/java/org/olat/commons/info/InfoMessageFrontendManagerTest.java
@@ -0,0 +1,344 @@
+/**
+ * <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.commons.info;
+
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+
+import java.util.List;
+import java.util.Random;
+import java.util.UUID;
+
+import org.junit.Assert;
+import org.junit.Test;
+import org.olat.commons.info.manager.InfoMessageFrontendManager;
+import org.olat.commons.info.model.InfoMessage;
+import org.olat.core.commons.persistence.DB;
+import org.olat.core.commons.services.notifications.NotificationsManager;
+import org.olat.core.commons.services.notifications.Publisher;
+import org.olat.core.commons.services.notifications.PublisherData;
+import org.olat.core.commons.services.notifications.SubscriptionContext;
+import org.olat.core.id.Identity;
+import org.olat.core.id.OLATResourceable;
+import org.olat.group.BusinessGroup;
+import org.olat.group.BusinessGroupService;
+import org.olat.repository.RepositoryEntry;
+import org.olat.test.JunitTestHelper;
+import org.olat.test.OlatTestCase;
+import org.springframework.beans.factory.annotation.Autowired;
+
+/**
+ * Initial Date: 20.03.2017
+ * 
+ * @author fkiefer, fabian.kiefer@frentix.com, www.frentix.com
+ */
+public class InfoMessageFrontendManagerTest extends OlatTestCase {
+
+	@Autowired
+	private NotificationsManager notificationManager;
+	@Autowired
+	private InfoMessageFrontendManager infoManager;
+	@Autowired
+	private BusinessGroupService groupService;
+	@Autowired
+	private DB dbInstance;
+
+	@Test
+	public void createSaveLoadAndCountInfoMessage() {
+		// same methods as already tested @InfoManagerTest
+		Identity id1 = JunitTestHelper.createAndPersistIdentityAsRndUser("info-1");
+		Identity id2 = JunitTestHelper.createAndPersistIdentityAsRndUser("info-2");
+		Identity id3 = JunitTestHelper.createAndPersistIdentityAsRndUser("info-3");
+		final String resName = UUID.randomUUID().toString();
+		Random random = new Random();
+		final InfoOLATResourceable ores = new InfoOLATResourceable(random.nextLong(), resName);
+		// create, save
+		InfoMessage msg1 = infoManager.createInfoMessage(ores, InfoMessageFrontendManager.businessGroupResSubPath, null,
+				id2);
+		msg1.setTitle("title-1");
+		msg1.setMessage("message-1");
+		assertNotNull(msg1);
+		infoManager.saveInfoMessage(msg1);
+		// create, save
+		InfoMessage msg2 = infoManager.createInfoMessage(ores, InfoMessageFrontendManager.businessGroupResSubPath, null,
+				id3);
+		msg2.setTitle("title-2");
+		msg2.setMessage("message-2");
+		assertNotNull(msg2);
+		infoManager.saveInfoMessage(msg2);
+		// create, not save
+		InfoMessage msg3 = infoManager.createInfoMessage(ores, InfoMessageFrontendManager.businessGroupResSubPath, null,
+				id1);
+		msg3.setTitle("title-3");
+		msg3.setMessage("message-3");
+		assertNotNull(msg3);
+		infoManager.saveInfoMessage(msg3);
+
+		dbInstance.commitAndCloseSession();
+
+		// load by key
+		InfoMessage loadedMsg1 = infoManager.loadInfoMessage(msg1.getKey());
+		assertNotNull(loadedMsg1);
+		InfoMessage loadedMsg2 = infoManager.loadInfoMessage(msg2.getKey());
+		assertNotNull(loadedMsg2);
+		InfoMessage loadedMsg3 = infoManager.loadInfoMessage(msg3.getKey());
+		assertNotNull(loadedMsg3);
+
+		// load by resource
+		List<InfoMessage> loadedMessages = infoManager.loadInfoMessageByResource(ores,
+				InfoMessageFrontendManager.businessGroupResSubPath, null, null, null, 0, 0);
+		assertNotNull(loadedMessages);
+		Assert.assertEquals(3, loadedMessages.size());
+		Assert.assertTrue(loadedMessages.contains(msg1));
+		Assert.assertTrue(loadedMessages.contains(msg2));
+		Assert.assertTrue(loadedMessages.contains(msg3));
+
+		// count info messages
+		int count = infoManager.countInfoMessageByResource(ores, null, null, null, null);
+		Assert.assertEquals(3, count);
+	}
+
+	@Test
+	public void deleteInfoMessage() {
+		Identity id1 = JunitTestHelper.createAndPersistIdentityAsRndUser("info-1");
+		Identity id2 = JunitTestHelper.createAndPersistIdentityAsRndUser("info-2");
+		final String resName = UUID.randomUUID().toString();
+		final InfoOLATResourceable ores = new InfoOLATResourceable(5l, resName);
+		// create, save
+		InfoMessage msg1 = infoManager.createInfoMessage(ores, InfoMessageFrontendManager.businessGroupResSubPath, null,
+				id2);
+		msg1.setTitle("title-1");
+		msg1.setMessage("message-1");
+		assertNotNull(msg1);
+		infoManager.saveInfoMessage(msg1);
+		// create, save
+		InfoMessage msg2 = infoManager.createInfoMessage(ores, InfoMessageFrontendManager.businessGroupResSubPath, null,
+				id1);
+		msg2.setTitle("title-2");
+		msg2.setMessage("message-2");
+		assertNotNull(msg2);
+		infoManager.saveInfoMessage(msg2);
+
+		dbInstance.commitAndCloseSession();
+
+		infoManager.deleteInfoMessage(msg1);
+		dbInstance.commitAndCloseSession();
+
+		InfoMessage loadedMsg1 = infoManager.loadInfoMessage(msg1.getKey());
+		Assert.assertNull(loadedMsg1);
+		InfoMessage loadedMsg2 = infoManager.loadInfoMessage(msg2.getKey());
+		assertNotNull(loadedMsg2);
+	}
+
+	@Test
+	public void deleteInfoMessagesOfIdentity() {
+		Identity id1 = JunitTestHelper.createAndPersistIdentityAsRndUser("info-1");
+		Identity id2 = JunitTestHelper.createAndPersistIdentityAsRndUser("info-2");
+		RepositoryEntry resource = JunitTestHelper.createAndPersistRepositoryEntry();
+		BusinessGroup businessGroup = groupService.createBusinessGroup(null, "gdao1", "gdao1-desc", -1, -1, false,
+				false, resource);
+		final OLATResourceable ores = new OLATResourceable() {
+			@Override
+			public String getResourceableTypeName() {
+				return businessGroup.getResourceableTypeName();
+			}
+
+			@Override
+			public Long getResourceableId() {
+				return businessGroup.getResourceableId();
+			}
+		};
+
+		// create, save
+		InfoMessage msg1 = infoManager.createInfoMessage(ores, InfoMessageFrontendManager.businessGroupResSubPath, null,
+				id2);
+		msg1.setTitle("title-1");
+		msg1.setMessage("message-1");
+		assertNotNull(msg1);
+		infoManager.saveInfoMessage(msg1);
+		// create, save
+		InfoMessage msg2 = infoManager.createInfoMessage(ores, InfoMessageFrontendManager.businessGroupResSubPath, null,
+				id1);
+		msg2.setTitle("title-2");
+		msg2.setMessage("message-2");
+		assertNotNull(msg2);
+		infoManager.saveInfoMessage(msg2);
+		// create, save
+		InfoMessage msg3 = infoManager.createInfoMessage(ores, InfoMessageFrontendManager.businessGroupResSubPath, null,
+				id1);
+		msg3.setTitle("title-3");
+		msg3.setMessage("message-3");
+		assertNotNull(msg3);
+		infoManager.saveInfoMessage(msg3);
+
+		dbInstance.commitAndCloseSession();
+
+		infoManager.deleteInfoMessagesOfIdentity(businessGroup, id1);
+		dbInstance.commitAndCloseSession();
+
+		// load messages after deletion
+		List<InfoMessage> loadedMessages = infoManager.loadInfoMessageByResource(ores,
+				InfoMessageFrontendManager.businessGroupResSubPath, null, null, null, 0, 0);
+		Assert.assertEquals(1, loadedMessages.size());
+		Assert.assertTrue(loadedMessages.contains(msg1));
+		Assert.assertFalse(loadedMessages.contains(msg2));
+		Assert.assertFalse(loadedMessages.contains(msg3));
+	}
+
+	@Test
+	public void removeInfoMessagesAndSubscriptionContext() {
+		Identity id1 = JunitTestHelper.createAndPersistIdentityAsRndUser("info-1");
+		Identity id2 = JunitTestHelper.createAndPersistIdentityAsRndUser("info-2");
+		RepositoryEntry resource = JunitTestHelper.createAndPersistRepositoryEntry();
+		BusinessGroup businessGroup = groupService.createBusinessGroup(null, "gdao1", "gdao1-desc", -1, -1, false,
+				false, resource);
+		final OLATResourceable ores = new OLATResourceable() {
+			@Override
+			public String getResourceableTypeName() {
+				return businessGroup.getResourceableTypeName();
+			}
+
+			@Override
+			public Long getResourceableId() {
+				return businessGroup.getResourceableId();
+			}
+		};		
+		// create, save
+		InfoMessage msg1 = infoManager.createInfoMessage(ores, InfoMessageFrontendManager.businessGroupResSubPath, null,
+				id2);
+		msg1.setTitle("title-1");
+		msg1.setMessage("message-1");
+		assertNotNull(msg1);
+		infoManager.saveInfoMessage(msg1);
+		// create, save
+		InfoMessage msg2 = infoManager.createInfoMessage(ores, InfoMessageFrontendManager.businessGroupResSubPath, null,
+				id1);
+		msg2.setTitle("title-2");
+		msg2.setMessage("message-2");
+		assertNotNull(msg2);
+		infoManager.saveInfoMessage(msg2);
+		// create, save
+		InfoMessage msg3 = infoManager.createInfoMessage(ores, InfoMessageFrontendManager.businessGroupResSubPath, null,
+				id1);
+		msg3.setTitle("title-3");
+		msg3.setMessage("message-3");
+		assertNotNull(msg3);
+		infoManager.saveInfoMessage(msg3);
+
+		dbInstance.commitAndCloseSession();
+		
+		SubscriptionContext sc = new SubscriptionContext(businessGroup.getResourceableTypeName(),
+				businessGroup.getResourceableId(), InfoMessageFrontendManager.businessGroupResSubPath);
+		PublisherData pd = new PublisherData("InfoMessage", "e.g. infoMessage=anyMessage", null);
+		// subscribe
+		notificationManager.subscribe(id1, sc, pd);
+		notificationManager.subscribe(id2, sc, pd);
+		dbInstance.closeSession();
+		
+		// check if publisher was created
+		Publisher p = notificationManager.getPublisher(sc);
+		assertNotNull(p);
+		
+		// check before message deletion
+		List<InfoMessage> loadedMessages1 = infoManager.loadInfoMessageByResource(ores,
+				InfoMessageFrontendManager.businessGroupResSubPath, null, null, null, 0, 0);
+		Assert.assertEquals(3, loadedMessages1.size());
+		Assert.assertTrue(loadedMessages1.contains(msg1));
+		Assert.assertTrue(loadedMessages1.contains(msg2));
+		Assert.assertTrue(loadedMessages1.contains(msg3));
+		// delete
+		infoManager.removeInfoMessagesAndSubscriptionContext(businessGroup);
+		dbInstance.commitAndCloseSession();
+		// check if messages are deleted
+		List<InfoMessage> loadedMessages2 = infoManager.loadInfoMessageByResource(ores,
+				InfoMessageFrontendManager.businessGroupResSubPath, null, null, null, 0, 0);
+		Assert.assertEquals(0, loadedMessages2.size());
+		Assert.assertFalse(loadedMessages2.contains(msg1));
+		Assert.assertFalse(loadedMessages2.contains(msg2));
+		Assert.assertFalse(loadedMessages2.contains(msg3));
+		// check if pubisher is deleted
+		Publisher p2 = notificationManager.getPublisher(sc);
+		assertNull("publisher marked deleted should not be found", p2);
+
+	}
+
+	@Test
+	public void sendInfoMessage() {
+		// TODO
+	}
+
+	@Test
+	public void getInfoSubscribers() {
+		Identity id1 = JunitTestHelper.createAndPersistIdentityAsRndUser("info-1");
+		Identity id2 = JunitTestHelper.createAndPersistIdentityAsRndUser("info-2");
+		RepositoryEntry resource = JunitTestHelper.createAndPersistRepositoryEntry();
+		BusinessGroup businessGroup = groupService.createBusinessGroup(null, "gdao", "gdao-desc", -1, -1, false, false,
+				resource);
+		final OLATResourceable ores = new OLATResourceable() {
+			@Override
+			public String getResourceableTypeName() {
+				return businessGroup.getResourceableTypeName();
+			}
+
+			@Override
+			public Long getResourceableId() {
+				return businessGroup.getResourceableId();
+			}
+		};
+		// create publisher data
+		String identifier = InfoMessageFrontendManager.businessGroupResSubPath;
+		SubscriptionContext context = new SubscriptionContext(businessGroup.getResourceableTypeName(),
+				businessGroup.getResourceableId(), identifier);
+		PublisherData publisherData = new PublisherData("testGetSubscriberIdentities", "e.g. data=infomessage", null);
+		dbInstance.commitAndCloseSession();
+
+		// add subscribers
+		notificationManager.subscribe(id1, context, publisherData);
+		notificationManager.subscribe(id2, context, publisherData);
+		dbInstance.commitAndCloseSession();
+
+		// get identities
+		List<Identity> identities = infoManager.getInfoSubscribers(ores, identifier);
+		Assert.assertNotNull(identities);
+		Assert.assertEquals(2, identities.size());
+		Assert.assertTrue(identities.contains(id1));
+		Assert.assertTrue(identities.contains(id2));
+	}
+
+	private class InfoOLATResourceable implements OLATResourceable {
+		private final Long resId;
+		private final String resName;
+
+		public InfoOLATResourceable(Long resId, String resName) {
+			this.resId = resId;
+			this.resName = resName;
+		}
+
+		@Override
+		public String getResourceableTypeName() {
+			return resName;
+		}
+
+		@Override
+		public Long getResourceableId() {
+			return resId;
+		}
+	}
+}
-- 
GitLab