/** * OLAT - Online Learning and Training<br> * http://www.olat.org * <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 * <p> * http://www.apache.org/licenses/LICENSE-2.0 * <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> * Copyright (c) since 2004 at Multimedia- & E-Learning Services (MELS),<br> * University of Zurich, Switzerland. * <hr> * <a href="http://www.openolat.org"> * OpenOLAT - Online Learning and Training</a><br> * This file has been modified by the OpenOLAT community. Changes are licensed * under the Apache 2.0 license as the original file. */ package org.olat.core.commons.services.notifications.manager; import java.io.File; import java.util.ArrayList; import java.util.Calendar; import java.util.Collection; import java.util.Collections; import java.util.Date; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Set; import javax.persistence.EntityManager; import javax.persistence.LockModeType; import javax.persistence.TypedQuery; import org.hibernate.FlushMode; import org.olat.NewControllerFactory; import org.olat.basesecurity.BaseSecurity; import org.olat.basesecurity.IdentityRef; import org.olat.core.CoreSpringFactory; import org.olat.core.commons.persistence.DB; import org.olat.core.commons.persistence.DBQuery; import org.olat.core.commons.persistence.PersistenceHelper; import org.olat.core.commons.services.notifications.NotificationHelper; import org.olat.core.commons.services.notifications.NotificationsHandler; 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.Subscriber; import org.olat.core.commons.services.notifications.SubscriptionContext; import org.olat.core.commons.services.notifications.SubscriptionInfo; import org.olat.core.commons.services.notifications.SubscriptionItem; import org.olat.core.commons.services.notifications.model.NoSubscriptionInfo; import org.olat.core.commons.services.notifications.model.PublisherImpl; import org.olat.core.commons.services.notifications.model.SubscriberImpl; import org.olat.core.commons.services.notifications.ui.NotificationSubscriptionController; import org.olat.core.gui.translator.Translator; import org.olat.core.helpers.Settings; import org.olat.core.id.Identity; import org.olat.core.id.OLATResourceable; import org.olat.core.id.Roles; import org.olat.core.id.context.BusinessControlFactory; import org.olat.core.logging.AssertException; import org.olat.core.logging.OLog; import org.olat.core.logging.Tracing; import org.olat.core.util.StringHelper; import org.olat.core.util.Util; import org.olat.core.util.WorkThreadInformations; import org.olat.core.util.coordinate.CoordinatorManager; import org.olat.core.util.coordinate.SyncerCallback; import org.olat.core.util.event.EventFactory; import org.olat.core.util.event.GenericEventListener; import org.olat.core.util.event.MultiUserEvent; import org.olat.core.util.i18n.I18nManager; import org.olat.core.util.mail.MailBundle; import org.olat.core.util.mail.MailManager; import org.olat.core.util.mail.MailerResult; import org.olat.core.util.resource.OresHelper; import org.olat.properties.Property; import org.olat.properties.PropertyManager; import org.olat.user.UserDataDeletable; /** * Description: <br> * see org.olat.core.commons.services.notifications.NotificationsManager * * Initial Date: 21.10.2004 <br> * @author Felix Jost */ public class NotificationsManagerImpl extends NotificationsManager implements UserDataDeletable { private static final OLog log = Tracing.createLoggerFor(NotificationsManagerImpl.class); private static final int PUB_STATE_OK = 0; private static final int PUB_STATE_NOT_OK = 1; private static final int BATCH_SIZE = 500; private static final String LATEST_EMAIL_USER_PROP = "noti_latest_email"; private final SubscriptionInfo NOSUBSINFO = new NoSubscriptionInfo(); private final OLATResourceable oresMyself = OresHelper.lookupType(NotificationsManagerImpl.class); private Map<String, NotificationsHandler> notificationHandlers; private List<String> notificationIntervals; private String defaultNotificationInterval; private static final Map<String, Integer> INTERVAL_DEF_MAP = buildIntervalMap(); private Object lockObject = new Object(); private DB dbInstance; private BaseSecurity securityManager; private PropertyManager propertyManager; /** * [used by spring] * @param userDeletionManager */ private NotificationsManagerImpl() { // private since singleton INSTANCE = this; } /** * [used by Spring] * @param dbInstance */ public void setDbInstance(DB dbInstance) { this.dbInstance = dbInstance; } /** * [user by Spring] * @param securityManager */ public void setSecurityManager(BaseSecurity securityManager) { this.securityManager = securityManager; } /** * [used by Spring] * @param propertyManager */ public void setPropertyManager(PropertyManager propertyManager) { this.propertyManager = propertyManager; } /** * @param resName * @param resId * @param subidentifier * @param type * @param data * @return a persisted publisher with ores/subidentifier as the composite * primary key */ private Publisher createAndPersistPublisher(String resName, Long resId, String subidentifier, String type, String data, String businessPath) { if (resName == null || resId == null || subidentifier == null) throw new AssertException( "resName, resId, and subidentifier must not be null"); if(businessPath != null && businessPath.length() > 230) { log.error("Businesspath too long for publisher: " + resName + " with business path: " + businessPath); businessPath = businessPath.substring(0, 230); } PublisherImpl pi = new PublisherImpl(resName, resId, subidentifier, type, data, businessPath, new Date(), PUB_STATE_OK); pi.setCreationDate(new Date()); dbInstance.getCurrentEntityManager().persist(pi); return pi; } /** * @param persistedPublisher * @param listener * @param subscriptionContext the context of the object we subscribe to * @return a subscriber with a db key */ protected Subscriber doCreateAndPersistSubscriber(Publisher persistedPublisher, Identity listener) { SubscriberImpl si = new SubscriberImpl(persistedPublisher, listener); si.setCreationDate(new Date()); si.setLastModified(new Date()); si.setLatestEmailed(new Date()); dbInstance.getCurrentEntityManager().persist(si); return si; } /** * subscribers for ONE person (e.g. subscribed to 5 forums -> 5 subscribers * belonging to this person) * * @param identity * @return List of Subscriber Objects which belong to the identity */ @Override public List<Subscriber> getSubscribers(Identity identity) { return getSubscribers(identity, Collections.<String>emptyList()); } /** * subscribers for ONE person (e.g. subscribed to 5 forums -> 5 subscribers * belonging to this person) restricted to the specified types * * @param identity * @return List of Subscriber Objects which belong to the identity */ @Override public List<Subscriber> getSubscribers(IdentityRef identity, List<String> types) { if(identity == null) return Collections.emptyList(); StringBuilder sb = new StringBuilder(); sb.append("select sub from notisub as sub ") .append("inner join fetch sub.publisher as publisher ") .append("where sub.identity.key = :identityKey"); if(types != null && !types.isEmpty()) { sb.append(" and publisher.type in (:types)"); } TypedQuery<Subscriber> query = dbInstance.getCurrentEntityManager() .createQuery(sb.toString(), Subscriber.class) .setParameter("identityKey", identity.getKey()); if(types != null && !types.isEmpty()) { query.setParameter("types", types); } return query.getResultList(); } /** * subscribers for ONE person (e.g. subscribed to 5 forums -> 5 subscribers * belonging to this person) restricted to the specified Olat resourceable id * * @param identity * @param resId * @return List of Subscriber Objects which belong to the identity */ @Override public List<Subscriber> getSubscribers(IdentityRef identity, long resId) { if(identity == null) return Collections.emptyList(); StringBuilder sb = new StringBuilder(); sb.append("select sub from notisub as sub ") .append("inner join fetch sub.publisher as publisher ") .append("where sub.identity.key = :identityKey") .append(" and publisher.resId = :resId)"); return dbInstance.getCurrentEntityManager() .createQuery(sb.toString(), Subscriber.class) .setParameter("identityKey", identity.getKey()) .setParameter("resId", resId) .getResultList(); } /** * @param identity * @return a list of all subscribers which belong to the identity and which * publishers are valid */ @Override public List<Subscriber> getValidSubscribers(Identity identity) { if(identity == null) return Collections.emptyList(); StringBuilder q = new StringBuilder(); q.append("select sub from notisub sub ") .append(" inner join fetch sub.publisher as pub ") .append(" where sub.identity.key=:anIdentityKey and pub.state=").append(PUB_STATE_OK); return dbInstance.getCurrentEntityManager() .createQuery(q.toString(), Subscriber.class) .setParameter("anIdentityKey", identity.getKey()) .getResultList(); } /** * @see org.olat.core.commons.services.notifications.NotificationsManager#getValidSubscribersOf(org.olat.core.commons.services.notifications.Publisher) */ @Override public List<Subscriber> getValidSubscribersOf(Publisher publisher) { StringBuilder q = new StringBuilder(); q.append("select sub from notisub sub ") .append(" inner join fetch sub.identity") .append(" where sub.publisher = :publisher and sub.publisher.state=").append(PUB_STATE_OK); return dbInstance.getCurrentEntityManager() .createQuery(q.toString(), Subscriber.class) .setParameter("publisher", publisher) .getResultList(); } @Override public List<SubscriptionInfo> getSubscriptionInfos(Identity identity, String publisherType) { StringBuilder sb = new StringBuilder(); sb.append("select sub from notisub sub") .append(" inner join fetch sub.publisher as pub") .append(" where sub.identity=:identity and pub.type=:type and pub.state=:aState"); List<Subscriber> subscribers = dbInstance.getCurrentEntityManager() .createQuery(sb.toString(), Subscriber.class) .setParameter("aState", PUB_STATE_OK) .setParameter("type", publisherType) .setParameter("identity", identity) .getResultList(); if(subscribers.isEmpty()) { return Collections.emptyList(); } Locale locale = new Locale(identity.getUser().getPreferences().getLanguage()); Date compareDate = getDefaultCompareDate(); List<SubscriptionInfo> sis = new ArrayList<SubscriptionInfo>(); for(Subscriber subscriber : subscribers){ Publisher pub = subscriber.getPublisher(); NotificationsHandler notifHandler = getNotificationsHandler(pub); // do not create subscription item when deleted if (isPublisherValid(pub)) { SubscriptionInfo subsInfo = notifHandler.createSubscriptionInfo(subscriber, locale, compareDate); if (subsInfo.hasNews()) { sis.add(subsInfo); } } } return sis; } @Override public void notifyAllSubscribersByEmail() { logAudit("starting notification cronjob to send email", null); WorkThreadInformations.setLongRunningTask("sendNotifications"); int counter = 0; int closeConnection = 0; List<Identity> identities; do { identities = securityManager.loadVisibleIdentities(counter, BATCH_SIZE); for(Identity identity:identities) { if(identity.getName().startsWith("guest_")) { Roles roles = securityManager.getRoles(identity); if(roles.isGuestOnly()) { continue; } } closeConnection++; processSubscribersByEmail(identity); if(closeConnection % 20 == 0) { dbInstance.commitAndCloseSession(); } } counter += identities.size(); dbInstance.commitAndCloseSession(); } while(identities.size() == BATCH_SIZE); // done, purge last entry WorkThreadInformations.unsetLongRunningTask("sendNotifications"); logAudit("end notification cronjob to send email", null); } private void processSubscribersByEmail(Identity ident) { if(ident.getStatus().compareTo(Identity.STATUS_VISIBLE_LIMIT) >= 0) { return;//send only to active user } String userInterval = getUserIntervalOrDefault(ident); if("never".equals(userInterval)) { return; } long start = System.currentTimeMillis(); Date compareDate = getCompareDateFromInterval(userInterval); Property p = propertyManager.findProperty(ident, null, null, null, LATEST_EMAIL_USER_PROP); if(p != null) { Date latestEmail = new Date(p.getLongValue()); if(latestEmail.after(compareDate)) { return;//nothing to do } } Date defaultCompareDate = getDefaultCompareDate(); List<Subscriber> subscribers = getSubscribers(ident); if(subscribers.isEmpty()) { return; } String langPrefs = null; if(ident.getUser() != null && ident.getUser().getPreferences() != null) { langPrefs = ident.getUser().getPreferences().getLanguage(); } Locale locale = I18nManager.getInstance().getLocaleOrDefault(langPrefs); boolean veto = false; Subscriber latestSub = null; List<SubscriptionItem> items = new ArrayList<>(); List<Subscriber> subsToUpdate = new ArrayList<>(); for(Subscriber sub:subscribers) { Date latestEmail = sub.getLatestEmailed(); SubscriptionItem subsitem = null; if (latestEmail == null || compareDate.after(latestEmail)){ // no notif. ever sent until now if (latestEmail == null) { latestEmail = defaultCompareDate; } else if (latestEmail.before(defaultCompareDate)) { //no notification older than a month latestEmail = defaultCompareDate; } subsitem = createSubscriptionItem(sub, locale, SubscriptionInfo.MIME_HTML, SubscriptionInfo.MIME_HTML, latestEmail); } else if(latestEmail != null && latestEmail.after(compareDate)) { //already send an email within the user's settings interval //veto = true; } if (subsitem != null) { items.add(subsitem); subsToUpdate.add(sub); } latestSub = sub; } Translator translator = Util.createPackageTranslator(NotificationSubscriptionController.class, locale); notifySubscribersByEmail(latestSub, items, subsToUpdate, translator, start, veto); } private void notifySubscribersByEmail(Subscriber latestSub, List<SubscriptionItem> items, List<Subscriber> subsToUpdate, Translator translator, long start, boolean veto) { if(veto) { if(latestSub != null) { logAudit(latestSub.getIdentity().getName() + " already received notification email within prefs interval"); } } else if (items.size() > 0) { Identity curIdent = latestSub.getIdentity(); boolean sentOk = sendMailToUserAndUpdateSubscriber(curIdent, items, translator, subsToUpdate); if (sentOk) { Property p = propertyManager.findProperty(curIdent, null, null, null, LATEST_EMAIL_USER_PROP); if(p == null) { p = propertyManager.createUserPropertyInstance(curIdent, null, LATEST_EMAIL_USER_PROP, null, null, null, null); p.setLongValue(new Date().getTime()); propertyManager.saveProperty(p); } else { p.setLongValue(new Date().getTime()); propertyManager.updateProperty(p); } StringBuilder mailLog = new StringBuilder(); mailLog.append("Notifications mailed for ").append(curIdent.getName()).append(' ').append(items.size()).append(' ').append((System.currentTimeMillis() - start)).append("ms"); logAudit(mailLog.toString()); } else { logAudit("Error sending notification email to : " + curIdent.getName()); } } //collecting the SubscriptionItem can potentially make a lot of DB calls dbInstance.intermediateCommit(); } /** * @see org.olat.core.commons.services.notifications.NotificationsManager#getCompareDateFromInterval(java.lang.String) */ public Date getCompareDateFromInterval(String interval){ Calendar calNow = Calendar.getInstance(); // get hours to subtract from now Integer diffHours = INTERVAL_DEF_MAP.get(interval); calNow.add(Calendar.HOUR_OF_DAY, -diffHours); Date compareDate = calNow.getTime(); return compareDate; } /** * Needs to correspond to notification-settings. * all available configs should be contained in the map below! * @return */ private static final Map<String, Integer> buildIntervalMap(){ Map<String, Integer> intervalDefMap = new HashMap<String, Integer>(); intervalDefMap.put("never", 0); intervalDefMap.put("monthly", 720); intervalDefMap.put("weekly", 168); intervalDefMap.put("daily", 24); intervalDefMap.put("half-daily", 12); intervalDefMap.put("four-hourly", 4); intervalDefMap.put("two-hourly", 2); return intervalDefMap; } /** * @see org.olat.core.commons.services.notifications.NotificationsManager#getUserIntervalOrDefault(org.olat.core.id.Identity) */ @Override public String getUserIntervalOrDefault(Identity ident){ if(ident == null || ident.getUser() == null || ident.getUser().getPreferences() == null) { logWarn("User " + (ident == null ? "NULL" : ident.getName()) + " has no preferences invalid", null); return getDefaultNotificationInterval(); } String userInterval = ident.getUser().getPreferences().getNotificationInterval(); if (!StringHelper.containsNonWhitespace(userInterval)) userInterval = getDefaultNotificationInterval(); List<String> avIntvls = getEnabledNotificationIntervals(); if (!avIntvls.contains(userInterval)) { logWarn("User " + ident.getName() + " has an invalid notification-interval (not found in config): " + userInterval, null); userInterval = getDefaultNotificationInterval(); } return userInterval; } @Override public boolean sendMailToUserAndUpdateSubscriber(Identity curIdent, List<SubscriptionItem> items, Translator translator, List<Subscriber> subscribersToUpdate) { boolean sentOk = sendEmail(curIdent, translator, items); // save latest email sent date for the subscription just emailed // do this only if the mail was successfully sent if (sentOk) { updateSubscriberLatestEmail(subscribersToUpdate); } return sentOk; } protected void updateSubscriberLatestEmail(List<Subscriber> subscribersToUpdate) { if(subscribersToUpdate == null || subscribersToUpdate.isEmpty()) { return;//nothing to do } StringBuilder q = new StringBuilder(); q.append("select sub from notisub sub ") .append(" inner join fetch sub.publisher where sub.key in (:aKey)"); EntityManager em = dbInstance.getCurrentEntityManager(); List<Long> keys = PersistenceHelper.toKeys(subscribersToUpdate); List<Subscriber> subscribers = em.createQuery(q.toString(), Subscriber.class) .setParameter("aKey", keys) .getResultList(); for (Subscriber subscriber :subscribers) { subscriber.setLastModified(new Date()); subscriber.setLatestEmailed(new Date()); em.merge(subscriber); } } private boolean sendEmail(Identity to, Translator translator, List<SubscriptionItem> subItems) { String title = translator.translate("rss.title", new String[] { NotificationHelper.getFormatedName(to) }); StringBuilder htmlText = new StringBuilder(); htmlText.append("<style>"); htmlText.append(".o_m_sub h4 {margin: 0 0 10px 0;}"); htmlText.append(".o_m_sub ul {padding: 0 0 5px 20px; margin: 0;}"); htmlText.append(".o_m_sub ul li {padding: 0; margin: 1px 0;}"); htmlText.append(".o_m_go {padding: 5px 0 0 0}"); htmlText.append(".o_date {font-size: 90%; color: #888}"); htmlText.append(".o_m_footer {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'}"); htmlText.append("</style>"); for (Iterator<SubscriptionItem> it_subs = subItems.iterator(); it_subs.hasNext();) { SubscriptionItem subitem = it_subs.next(); // o_m_wrap class for overriding styles in master mail template htmlText.append("<div class='o_m_wrap'>"); // add background here for gmail as they ignore classes. htmlText.append("<div class='o_m_sub' style='background: #FAFAFA; padding: 5px 5px; margin: 10px 0;'>"); // 1: title htmlText.append(subitem.getTitle()); htmlText.append("\n"); // 2: content String desc = subitem.getDescription(); if(StringHelper.containsNonWhitespace(desc)) { htmlText.append(desc); } // 3: goto-link String link = subitem.getLink(); if(StringHelper.containsNonWhitespace(link)) { htmlText.append("<div class='o_m_go'><a href=\"").append(link).append("\">"); SubscriptionInfo subscriptionInfo = subitem.getSubsInfo(); if (subscriptionInfo != null) { String innerType = subscriptionInfo.getType(); String typeName = NewControllerFactory.translateResourceableTypeName(innerType, translator.getLocale()); String open = translator.translate("resource.open", new String[] { typeName }); htmlText.append(open); htmlText.append(" »</a></div>"); } } htmlText.append("\n"); htmlText.append("</div></div>"); } String basePath = Settings.getServerContextPathURI() + "/auth/HomeSite/" + to.getKey() + "/"; htmlText.append("<div class='o_m_footer'>"); htmlText.append(translator.translate("footer.notifications", new String[] {basePath + "mysettings/0", basePath += "notifications/0", basePath + "/tab/1"})); htmlText.append("</div>"); MailerResult result = null; try { MailBundle bundle = new MailBundle(); bundle.setToId(to); bundle.setContent(title, htmlText.toString()); result = CoreSpringFactory.getImpl(MailManager.class).sendExternMessage(bundle, null, true); } catch (Exception e) { // FXOLAT-294 :: sending the mail will throw nullpointer exception if To-Identity has no // valid email-address!, catch it... } if (result == null || result.getReturnCode() > 0) { if(result!=null) log.warn("Could not send email to identity " + to.getName() + ". (returncode=" + result.getReturnCode() + ", to=" + to + ")"); else log.warn("Could not send email to identity " + to.getName() + ". (returncode = null) , to=" + to + ")"); return false; } else { return true; } } /** * @param key * @return the subscriber with this key or null if not found */ @Override public Subscriber getSubscriber(Long key) { StringBuilder q = new StringBuilder(); q.append("select sub from notisub as sub") .append(" inner join fetch sub.publisher ") .append(" where sub.key=:aKey"); List<Subscriber> res = dbInstance.getCurrentEntityManager() .createQuery(q.toString(), Subscriber.class) .setParameter("aKey", key.longValue()) .getResultList(); if (res.isEmpty()) return null; if (res.size() > 1) throw new AssertException("more than one subscriber for key " + key); return res.get(0); } /** * @param scontext * @param pdata * @return the publisher */ public Publisher getOrCreatePublisher(final SubscriptionContext scontext, final PublisherData pdata) { return findOrCreatePublisher(scontext, pdata); } /** * @param scontext * @param pdata * @return the publisher */ private Publisher findOrCreatePublisher(final SubscriptionContext scontext, final PublisherData pdata) { final OLATResourceable ores = OresHelper.createOLATResourceableInstance(scontext.getResName() + "_" + scontext.getSubidentifier(),scontext.getResId()); //o_clusterOK by:cg //fxdiff VCRP-16:prevent nested doInSync Publisher pub = getPublisher(scontext); if(pub != null) { return pub; } Publisher publisher = CoordinatorManager.getInstance().getCoordinator().getSyncer().doInSync(ores, new SyncerCallback<Publisher>(){ public Publisher execute() { Publisher p = getPublisher(scontext); // if not found, create it if (p == null) { p = createAndPersistPublisher(scontext.getResName(), scontext.getResId(), scontext.getSubidentifier(), pdata.getType(), pdata .getData(), pdata.getBusinessPath()); } return p; } }); return publisher; } /** * @param subsContext * @return the publisher belonging to the given context or null */ @Override public Publisher getPublisher(SubscriptionContext subsContext) { StringBuilder q = new StringBuilder(); q.append("select pub from notipublisher pub ") .append(" where pub.resName=:resName and pub.resId = :resId"); if(StringHelper.containsNonWhitespace(subsContext.getSubidentifier())) { q.append(" and pub.subidentifier=:subidentifier"); } else { q.append(" and (pub.subidentifier='' or pub.subidentifier is null)"); } TypedQuery<Publisher> query = dbInstance.getCurrentEntityManager() .createQuery(q.toString(), Publisher.class) .setParameter("resName", subsContext.getResName()) .setParameter("resId", subsContext.getResId()); if(StringHelper.containsNonWhitespace(subsContext.getSubidentifier())) { query.setParameter("subidentifier", subsContext.getSubidentifier()); } List<Publisher> res = query.getResultList(); if (res.isEmpty()) return null; if (res.size() != 1) throw new AssertException("only one subscriber per person and publisher!!"); return res.get(0); } @Override public void updatePublisherData(SubscriptionContext subsContext, PublisherData data){ Publisher publisher= getPublisherForUpdate(subsContext); if(publisher != null){ publisher.setData(data.getData()); dbInstance.getCurrentEntityManager().merge(publisher); dbInstance.commit(); } } private Publisher getPublisherForUpdate(SubscriptionContext subsContext) { Publisher pub = getPublisher(subsContext); if(pub != null && pub.getKey() != null) { //prevent optimistic lock issue dbInstance.getCurrentEntityManager().detach(pub); pub = dbInstance.getCurrentEntityManager() .find(PublisherImpl.class, pub.getKey(), LockModeType.PESSIMISTIC_WRITE); } return pub; } @Override public List<Publisher> getAllPublisher() { String q = "select pub from notipublisher pub"; return dbInstance.getCurrentEntityManager().createQuery(q, Publisher.class) .getResultList(); } /** * @param resName * @param resId * @return a list of publishers belonging to the resource */ private List<Publisher> getPublishers(String resName, Long resId) { String q = "select pub from notipublisher pub where pub.resName=:resName and pub.resId= :resId"; return dbInstance.getCurrentEntityManager() .createQuery(q, Publisher.class) .setParameter("resName", resName) .setParameter("resId", resId.longValue()) .getResultList(); } /** * deletes all publishers of the given olatresourceable. e.g. ores = * businessgroup 123 -> deletes possible publishers: of Folder(toolfolder), of * Forum(toolforum) * * @param ores */ @Override public void deletePublishersOf(OLATResourceable ores) { String type = ores.getResourceableTypeName(); Long id = ores.getResourceableId(); if (type == null || id == null) throw new AssertException("type/id cannot be null! type:" + type + " / id:" + id); List<Publisher> pubs = getPublishers(type, id); if(pubs.isEmpty()) return; String q1 = "delete from notisub sub where sub.publisher in (:publishers)"; DBQuery query1 = dbInstance.createQuery(q1); query1.setParameterList("publishers", pubs); query1.executeUpdate(FlushMode.AUTO); String q2 = "delete from notipublisher pub where pub in (:publishers)"; DBQuery query2 = dbInstance.createQuery(q2); query2.setParameterList("publishers", pubs); query2.executeUpdate(FlushMode.AUTO); } /** * @param identity * @param publisher * @return a Subscriber object belonging to the identity and listening to the * given publisher */ @Override public Subscriber getSubscriber(Identity identity, Publisher publisher) { List<Subscriber> res = dbInstance.getCurrentEntityManager() .createNamedQuery("subscribersByPublisherAndIdentity", Subscriber.class) .setParameter("publisherKey", publisher.getKey()) .setParameter("identityKey", identity.getKey()) .getResultList(); if (res.size() == 0) return null; if (res.size() != 1) throw new AssertException("only one subscriber per person and publisher!!"); Subscriber s = res.get(0); return s; } /** * @see org.olat.core.commons.services.notifications.NotificationsManager#getSubscriber(org.olat.core.commons.services.notifications.Publisher) */ @Override public List<Subscriber> getSubscribers(Publisher publisher) { return dbInstance.getCurrentEntityManager() .createNamedQuery("subscribersByPublisher", Subscriber.class) .setParameter("publisher", publisher) .getResultList(); } /** * * @see org.olat.core.commons.services.notifications.NotificationsManager#getSubscriberIdentities(org.olat.core.commons.services.notifications.Publisher) */ @Override public List<Identity> getSubscriberIdentities(Publisher publisher) { return dbInstance.getCurrentEntityManager() .createNamedQuery("identitySubscribersByPublisher", Identity.class) .setParameter("publisher", publisher) .getResultList(); } /** * @return the handler for the type */ public NotificationsHandler getNotificationsHandler(Publisher publisher) { String type = publisher.getType(); if (notificationHandlers == null) { synchronized(lockObject) { if (notificationHandlers == null) { // check again in synchronized-block, only one may create list notificationHandlers = new HashMap<String,NotificationsHandler>(); Map<String, NotificationsHandler> notificationsHandlerMap = CoreSpringFactory.getBeansOfType(NotificationsHandler.class); Collection<NotificationsHandler> notificationsHandlerValues = notificationsHandlerMap.values(); for (NotificationsHandler notificationsHandler : notificationsHandlerValues) { log.debug("initNotificationUpgrades notificationsHandler=" + notificationsHandler); notificationHandlers.put(notificationsHandler.getType(), notificationsHandler); } } } } return notificationHandlers.get(type); } /** * @param subscriber */ private void deleteSubscriber(Subscriber subscriber) { dbInstance.deleteObject(subscriber); } public boolean deleteSubscriber(Long subscriberKey) { String sb = "delete from notisub sub where sub.key=:subscriberKey"; int rows = dbInstance.getCurrentEntityManager() .createQuery(sb) .setParameter("subscriberKey", subscriberKey) .executeUpdate(); return rows > 0; } /** * sets the latest visited date of the subscription to 'now' .assumes the * identity is already subscribed to the publisher * * @param identity * @param subsContext */ @Override public void markSubscriberRead(Identity identity, SubscriptionContext subsContext) { Publisher p = getPublisher(subsContext); if (p == null) throw new AssertException("cannot markRead for identity " + identity.getName() + ", since the publisher for the given subscriptionContext does not exist: subscontext = " + subsContext); markSubscriberRead(identity, p); } private Subscriber markSubscriberRead(Identity identity, Publisher p) { Subscriber sub = getSubscriber(identity, p); if(sub != null) { sub.setLastModified(new Date()); sub = dbInstance.getCurrentEntityManager().merge(sub); } return sub; } /** * @param identity * @param subscriptionContext * @param publisherData */ @Override public void subscribe(Identity identity, SubscriptionContext subscriptionContext, PublisherData publisherData) { //need to sync as opt-in is sometimes implemented Publisher toUpdate = getPublisherForUpdate(subscriptionContext); if(toUpdate == null) { //create the publisher findOrCreatePublisher(subscriptionContext, publisherData); //lock the publisher toUpdate = getPublisherForUpdate(subscriptionContext); } Subscriber s = getSubscriber(identity, toUpdate); if (s == null) { // no subscriber -> create. // s.latestReadDate >= p.latestNewsDate == no news for subscriber when no // news after subscription time doCreateAndPersistSubscriber(toUpdate, identity); } dbInstance.commit(); } @Override public void subscribe(List<Identity> identities, SubscriptionContext subscriptionContext, PublisherData publisherData) { if(identities == null || identities.isEmpty()) return; Publisher toUpdate = getPublisherForUpdate(subscriptionContext); if(toUpdate == null) { //create the publisher findOrCreatePublisher(subscriptionContext, publisherData); //lock the publisher toUpdate = getPublisherForUpdate(subscriptionContext); } for(Identity identity:identities) { Subscriber s = getSubscriber(identity, toUpdate); if (s == null) { // no subscriber -> create. // s.latestReadDate >= p.latestNewsDate == no news for subscriber when no // news after subscription time doCreateAndPersistSubscriber(toUpdate, identity); } } dbInstance.commit(); } /** * call this method to indicate that there is news for the given * subscriptionContext * * @param subscriptionContext * @param ignoreNewsFor */ @Override public void markPublisherNews(final SubscriptionContext subscriptionContext, Identity ignoreNewsFor, boolean sendEvents) { // to make sure: ignore if no subscriptionContext if (subscriptionContext == null) return; Publisher toUpdate = getPublisherForUpdate(subscriptionContext); if(toUpdate == null) { return; } toUpdate.setLatestNewsDate(new Date()); Publisher publisher = dbInstance.getCurrentEntityManager().merge(toUpdate); dbInstance.commit();//commit the select for update // no need to sync, since there is only one gui thread at a time from one // user if (ignoreNewsFor != null) { markSubscriberRead(ignoreNewsFor, publisher); } if(sendEvents) { //commit all things on the database dbInstance.commit(); // channel-notify all interested listeners (e.g. the pnotificationsportletruncontroller) // 1. find all subscribers which can be affected List<Subscriber> subscribers = getValidSubscribersOf(publisher); Set<Long> subsKeys = new HashSet<Long>(); // 2. collect all keys of the affected subscribers for (Iterator<Subscriber> it_subs = subscribers.iterator(); it_subs.hasNext();) { Subscriber su = it_subs.next(); subsKeys.add(su.getKey()); } // fire the event MultiUserEvent mue = EventFactory.createAffectedEvent(subsKeys); CoordinatorManager.getInstance().getCoordinator().getEventBus().fireEventToListenersOf(mue, oresMyself); } } /** * @see org.olat.core.commons.services.notifications.NotificationsManager#registerAsListener(org.olat.core.util.event.GenericEventListener, org.olat.core.id.Identity) */ @Override public void registerAsListener(GenericEventListener gel, Identity ident) { CoordinatorManager.getInstance().getCoordinator().getEventBus().registerFor(gel, ident, oresMyself); } /** * @see org.olat.core.commons.services.notifications.NotificationsManager#deregisterAsListener(org.olat.core.util.event.GenericEventListener) */ @Override public void deregisterAsListener(GenericEventListener gel) { CoordinatorManager.getInstance().getCoordinator().getEventBus().deregisterFor(gel, oresMyself); } /** * @param identity * @param subscriptionContext */ @Override public void unsubscribe(Identity identity, SubscriptionContext subscriptionContext) { Publisher p = getPublisherForUpdate(subscriptionContext); if (p != null) { Subscriber s = getSubscriber(identity, p); if (s != null) { deleteSubscriber(s); } else { logWarn("could not unsubscribe " + identity.getName() + " from publisher:" + p.getResName() + "," + p.getResId() + "," + p.getSubidentifier(), null); } } dbInstance.commit(); } @Override public void unsubscribe(List<Identity> identities, SubscriptionContext subscriptionContext) { if(identities == null || identities.isEmpty()) return; Publisher p = getPublisherForUpdate(subscriptionContext); if (p != null) { for(Identity identity:identities) { Subscriber s = getSubscriber(identity, p); if (s != null) { deleteSubscriber(s); } else { logWarn("could not unsubscribe " + identity.getName() + " from publisher:" + p.getResName() + "," + p.getResId() + "," + p.getSubidentifier(), null); } } } dbInstance.commit(); } /** * * @see org.olat.core.commons.services.notifications.NotificationsManager#unsubscribe(org.olat.core.commons.services.notifications.Subscriber) */ @Override public void unsubscribe(Subscriber s) { Subscriber foundSub = getSubscriber(s.getKey()); if (foundSub != null) { deleteSubscriber(foundSub); } else { logWarn("could not unsubscribe " + s.getIdentity().getName() + " from publisher:" + s.getPublisher().getResName() + "," + s.getPublisher().getResId() + "," + s.getPublisher().getSubidentifier(), null); } } @Override public void unsubscribeAllForIdentityAndResId(IdentityRef identity, Long resId) { List<Subscriber> subscribers = getSubscribers(identity, resId.longValue()); for (Subscriber sub:subscribers) { unsubscribe (sub); } } /** * @param identity * @param subscriptionContext * @return true if this user is subscribed */ @Override public boolean isSubscribed(Identity identity, SubscriptionContext subscriptionContext) { StringBuilder q = new StringBuilder(); q.append("select count(sub) from notisub as sub ") .append(" inner join sub.publisher as pub ") .append(" where sub.identity.key=:anIdentityKey and pub.resName=:resName and pub.resId=:resId") .append(" and pub.subidentifier=:subidentifier"); Number count = dbInstance.getCurrentEntityManager() .createQuery(q.toString(), Number.class) .setParameter("anIdentityKey", identity.getKey()) .setParameter("resName", subscriptionContext.getResName()) .setParameter("resId", subscriptionContext.getResId().longValue()) .setParameter("subidentifier", subscriptionContext.getSubidentifier()) .getSingleResult(); long cnt = count.longValue(); if (cnt == 0) return false; else return true; } /** * delete publisher and subscribers * * @param scontext the subscriptioncontext */ @Override public void delete(SubscriptionContext scontext) { Publisher p = getPublisher(scontext); // if none found, no one has subscribed yet and therefore no publisher has // been generated lazily. // -> nothing to do if (p == null) return; //first delete all subscribers List<Subscriber> subscribers = getValidSubscribersOf(p); for (Subscriber subscriber : subscribers) { deleteSubscriber(subscriber); } // else: dbInstance.deleteObject(p); } /** * delete publisher and subscribers * * @param publisher the publisher to delete */ @Override public void deactivate(Publisher publisher) { EntityManager em = dbInstance.getCurrentEntityManager(); PublisherImpl toDeactivate = em.find(PublisherImpl.class, publisher.getKey(), LockModeType.PESSIMISTIC_WRITE); toDeactivate.setState(PUB_STATE_NOT_OK); em.merge(toDeactivate); dbInstance.commit(); } /** * @param pub * @return true if the publisher is valid (that is: has not been marked as * deleted) */ public boolean isPublisherValid(Publisher pub) { return pub.getState() == PUB_STATE_OK; } /** * @param subscriber * @param locale * @param mimeType text/html or text/plain * @return the item or null if there is currently no news for this subscription */ public SubscriptionItem createSubscriptionItem(Subscriber subscriber, Locale locale, String mimeTypeTitle, String mimeTypeContent) { // calculate the item based on subscriber.getLastestReadDate() // used for rss-feed, no longer than 1 month Date compareDate = getDefaultCompareDate(); return createSubscriptionItem(subscriber, locale, mimeTypeTitle, mimeTypeContent, compareDate); } /** * if no compareDate is selected, cannot be calculated by user-interval, or no latestEmail is available => use this to get a Date 30d in the past. * * maybe the latest user-login could also be used. * @return Date */ private Date getDefaultCompareDate() { Calendar calNow = Calendar.getInstance(); calNow.add(Calendar.DAY_OF_MONTH, -30); Date compareDate = calNow.getTime(); return compareDate; } /** * * @param subscriber * @param locale * @param mimeType * @param latestEmailed needs to be given! SubscriptionInfo is collected from then until latestNews of publisher * @return null if the publisher is not valid anymore (deleted), or if there are no news */ @Override public SubscriptionItem createSubscriptionItem(Subscriber subscriber, Locale locale, String mimeTypeTitle, String mimeTypeContent, Date latestEmailed) { if (latestEmailed == null) throw new AssertException("compareDate may not be null, use a date from history"); try { boolean debug = isLogDebugEnabled(); SubscriptionItem si = null; Publisher pub = subscriber.getPublisher(); NotificationsHandler notifHandler = getNotificationsHandler(pub); if(debug) logDebug("create subscription with handler: " + notifHandler.getClass().getName()); // do not create subscription item when deleted if (isPublisherValid(pub) && notifHandler != null) { if(debug) logDebug("NotifHandler: " + notifHandler.getClass().getName() + " compareDate: " + latestEmailed.toString() + " now: " + new Date().toString(), null); SubscriptionInfo subsInfo = notifHandler.createSubscriptionInfo(subscriber, locale, latestEmailed); if (subsInfo.hasNews()) { si = createSubscriptionItem(subsInfo, subscriber, locale, mimeTypeTitle, mimeTypeContent); } } return si; } catch (Exception e) { log.error("Cannot generate a subscription item.", e); return null; } } @Override public SubscriptionItem createSubscriptionItem(SubscriptionInfo subsInfo, Subscriber subscriber, Locale locale, String mimeTypeTitle, String mimeTypeContent) { Publisher pub = subscriber.getPublisher(); String title = getFormatedTitle(subsInfo, subscriber, locale, mimeTypeTitle); String itemLink = null; if(subsInfo.getCustomUrl() != null) { itemLink = subsInfo.getCustomUrl(); } if(itemLink == null && pub.getBusinessPath() != null) { itemLink = BusinessControlFactory.getInstance().getURLFromBusinessPathString(pub.getBusinessPath()); } String description = subsInfo.getSpecificInfo(mimeTypeContent, locale); SubscriptionItem subscriptionItem = new SubscriptionItem(title, itemLink, description); subscriptionItem.setSubsInfo(subsInfo); return subscriptionItem; } /** * format the type-title and title-details * @param subscriber * @param locale * @param mimeType * @return */ private String getFormatedTitle(SubscriptionInfo subsInfo, Subscriber subscriber, Locale locale, String mimeType){ Publisher pub = subscriber.getPublisher(); StringBuilder titleSb = new StringBuilder(); String title = subsInfo.getTitle(mimeType); if (StringHelper.containsNonWhitespace(title)) { titleSb.append(title); } else { NotificationsHandler notifHandler = getNotificationsHandler(pub); String titleInfo = notifHandler.createTitleInfo(subscriber, locale); if (StringHelper.containsNonWhitespace(titleInfo)) { titleSb.append(titleInfo); } } return titleSb.toString(); } /** * * @see org.olat.core.commons.services.notifications.NotificationsManager#getNoSubscriptionInfo() */ public SubscriptionInfo getNoSubscriptionInfo() { return NOSUBSINFO; } /** * Delete all subscribers for certain identity. * @param identity */ @Override public void deleteUserData(Identity identity, String newDeletedUserName, File archivePath) { List<Subscriber> subscribers = getSubscribers(identity); for (Iterator<Subscriber> iter = subscribers.iterator(); iter.hasNext();) { deleteSubscriber( iter.next() ); } logDebug("All notification-subscribers deleted for identity=" + identity, null); } /** * Spring setter method * * @param notificationIntervals */ public void setNotificationIntervals(Map<String, Boolean> intervals) { notificationIntervals = new ArrayList<String>(); for(String key : intervals.keySet()) { if (intervals.get(key)) { if(key.length() <= 16) { notificationIntervals.add(key); } else { log.error("Interval notification cannot be more than 16 characters wide: " + key); } } } } /** * Spring setter method * * @param defaultNotificationInterval */ public void setDefaultNotificationInterval(String defaultNotificationInterval) { if (defaultNotificationInterval != null) { defaultNotificationInterval = defaultNotificationInterval.trim(); } this.defaultNotificationInterval = defaultNotificationInterval; } /** * @see org.olat.core.commons.services.notifications.NotificationsManager#getDefaultNotificationInterval() */ public String getDefaultNotificationInterval() { return defaultNotificationInterval; } /** * @see org.olat.core.commons.services.notifications.NotificationsManager#getNotificationIntervals() */ public List<String> getEnabledNotificationIntervals() { return notificationIntervals; } }