
org.openremote.manager.notification.NotificationService Maven / Gradle / Ivy
/*
* Copyright 2017, OpenRemote Inc.
*
* See the CONTRIBUTORS.txt file in the distribution for a
* full listing of individual contributors.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see .
*/
package org.openremote.manager.notification;
import jakarta.persistence.Query;
import jakarta.persistence.TypedQuery;
import org.apache.camel.builder.RouteBuilder;
import org.openremote.container.message.MessageBrokerService;
import org.openremote.container.persistence.PersistenceService;
import org.openremote.container.security.AuthContext;
import org.openremote.container.timer.TimerService;
import org.openremote.manager.asset.AssetStorageService;
import org.openremote.manager.security.ManagerIdentityService;
import org.openremote.manager.web.ManagerWebService;
import org.openremote.model.Constants;
import org.openremote.model.Container;
import org.openremote.model.ContainerService;
import org.openremote.model.asset.Asset;
import org.openremote.model.notification.Notification;
import org.openremote.model.notification.NotificationSendResult;
import org.openremote.model.notification.RepeatFrequency;
import org.openremote.model.notification.SentNotification;
import org.openremote.model.query.UserQuery;
import org.openremote.model.query.filter.StringPredicate;
import org.openremote.model.security.User;
import org.openremote.model.util.TextUtil;
import org.openremote.model.util.TimeUtil;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.*;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.atomic.AtomicReference;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import static java.time.temporal.ChronoUnit.*;
import static java.util.Map.entry;
import static org.openremote.manager.notification.NotificationProcessingException.Reason.*;
import static org.openremote.model.notification.Notification.HEADER_SOURCE;
import static org.openremote.model.notification.Notification.Source.*;
public class NotificationService extends RouteBuilder implements ContainerService {
public static final String NOTIFICATION_QUEUE = "direct://NotificationQueue";
private static final Logger LOG = Logger.getLogger(NotificationService.class.getName());
protected TimerService timerService;
protected PersistenceService persistenceService;
protected AssetStorageService assetStorageService;
protected ManagerIdentityService identityService;
protected MessageBrokerService messageBrokerService;
protected ExecutorService executorService;
protected Map notificationHandlerMap = new HashMap<>();
@Override
public int getPriority() {
return ContainerService.DEFAULT_PRIORITY;
}
@Override
public void init(Container container) throws Exception {
this.timerService = container.getService(TimerService.class);
this.persistenceService = container.getService(PersistenceService.class);
this.assetStorageService = container.getService(AssetStorageService.class);
this.identityService = container.getService(ManagerIdentityService.class);
this.messageBrokerService = container.getService(MessageBrokerService.class);
executorService = container.getExecutor();
container.getService(MessageBrokerService.class).getContext().addRoutes(this);
container.getServices(NotificationHandler.class).forEach(notificationHandler ->
notificationHandlerMap.put(notificationHandler.getTypeName(), notificationHandler));
container.getService(ManagerWebService.class).addApiSingleton(
new NotificationResourceImpl(this,
container.getService(MessageBrokerService.class),
container.getService(AssetStorageService.class),
container.getService(ManagerIdentityService.class))
);
}
@Override
public void start(Container container) throws Exception {
}
@Override
public void stop(Container container) throws Exception {
}
@Override
public void configure() throws Exception {
from(NOTIFICATION_QUEUE)
.routeId("NotificationQueue")
.threads().executorService(executorService)
.process(exchange -> {
Notification notification = exchange.getIn().getBody(Notification.class);
if (notification == null) {
throw new NotificationProcessingException(MISSING_NOTIFICATION, "Notification must be set");
}
LOG.finest("Processing: " + notification.getName());
if (notification.getMessage() == null) {
throw new NotificationProcessingException(MISSING_CONTENT, "Notification message must be set");
}
Notification.Source source = exchange.getIn().getHeader(HEADER_SOURCE, () -> null, Notification.Source.class);
if (source == null) {
throw new NotificationProcessingException(MISSING_SOURCE);
}
// Validate handler and message
NotificationHandler handler = notificationHandlerMap.get(notification.getMessage().getType());
if (handler == null) {
throw new NotificationProcessingException(UNSUPPORTED_MESSAGE_TYPE, "No handler for message type: " + notification.getMessage().getType());
}
if (!handler.isValid()) {
throw new NotificationProcessingException(NOTIFICATION_HANDLER_CONFIG_ERROR, "Handler is not valid: " + handler.getTypeName());
}
if (!handler.isMessageValid(notification.getMessage())) {
throw new NotificationProcessingException(INVALID_MESSAGE);
}
// Validate access and map targets to handler compatible targets
String realm = null;
String userId = null;
String assetId = null;
AtomicReference sourceId = new AtomicReference<>("");
boolean isSuperUser = false;
boolean isRestrictedUser = false;
switch (source) {
case INTERNAL -> isSuperUser = true;
case CLIENT -> {
AuthContext authContext = exchange.getIn().getHeader(Constants.AUTH_CONTEXT, AuthContext.class);
if (authContext == null) {
// Anonymous clients cannot send notifications
throw new NotificationProcessingException(INSUFFICIENT_ACCESS);
}
realm = authContext.getAuthenticatedRealmName();
userId = authContext.getUserId();
sourceId.set(userId);
isSuperUser = authContext.isSuperUser();
isRestrictedUser = identityService.getIdentityProvider().isRestrictedUser(authContext);
}
case GLOBAL_RULESET -> isSuperUser = true;
case REALM_RULESET -> {
realm = exchange.getIn().getHeader(Notification.HEADER_SOURCE_ID, String.class);
sourceId.set(realm);
}
case ASSET_RULESET -> {
assetId = exchange.getIn().getHeader(Notification.HEADER_SOURCE_ID, String.class);
sourceId.set(assetId);
Asset> asset = assetStorageService.find(assetId, false);
realm = asset.getRealm();
}
}
LOG.fine("Sending " + notification.getMessage().getType() + " notification '" + notification.getName() + "': '" + source + ":" + sourceId.get() + "' -> " + notification.getTargets());
// Check access permissions
checkAccess(source, sourceId.get(), notification.getTargets(), realm, userId, isSuperUser, isRestrictedUser, assetId);
// Get the list of notification targets
List mappedTargetsList = handler.getTargets(source, sourceId.get(), notification.getTargets(), notification.getMessage());
if (mappedTargetsList == null || mappedTargetsList.isEmpty()) {
throw new NotificationProcessingException(MISSING_TARGETS, "Notification targets must be set");
} else if (LOG.isLoggable(Level.FINER)) {
LOG.finer("Notification targets mapped from: [" + (notification.getTargets() != null ? notification.getTargets().stream().map(Object::toString).collect(Collectors.joining(",")) : "null") + "to: [" + mappedTargetsList.stream().map(Object::toString).collect(Collectors.joining(",")) + "]");
}
// Filter targets based on repeat frequency
if (!TextUtil.isNullOrEmpty(notification.getName()) && (!TextUtil.isNullOrEmpty(notification.getRepeatInterval()) || notification.getRepeatFrequency() != null)) {
mappedTargetsList = mappedTargetsList.stream()
.filter(target -> okToSendNotification(source, sourceId.get(), target, notification))
.collect(Collectors.toList());
}
// Send message to each applicable target
AtomicReference error = new AtomicReference<>();
// As we can have multiple targets in a single exchange we'll track the first exception that occurs
mappedTargetsList.forEach(
target -> {
Exception notificationError = persistenceService.doReturningTransaction(em -> {
// commit the notification first to get the ID
SentNotification sentNotification = new SentNotification()
.setName(notification.getName())
.setType(notification.getMessage().getType())
.setSource(source)
.setSourceId(sourceId.get())
.setTarget(target.getType())
.setTargetId(target.getId())
.setMessage(notification.getMessage())
.setSentOn(Date.from(timerService.getNow()));
sentNotification = em.merge(sentNotification);
long id = sentNotification.getId();
try {
handler.sendMessage(
id,
source,
sourceId.get(),
target,
notification.getMessage());
NotificationSendResult result = NotificationSendResult.success();
LOG.fine("Notification sent '" + id + "': " + target);
// Merge the sent notification again with the message included just in case the handler modified the message
sentNotification.setMessage(notification.getMessage());
} catch (Exception e) {
NotificationProcessingException notificationProcessingException;
if (e instanceof NotificationProcessingException) {
notificationProcessingException = (NotificationProcessingException) e;
} else {
notificationProcessingException = new NotificationProcessingException(SEND_FAILURE, e.getMessage());
}
LOG.warning("Notification failed '" + id + "': " + target + ", reason=" + notificationProcessingException);
sentNotification.setError(TextUtil.isNullOrEmpty(notificationProcessingException.getMessage()) ? "Unknown error" : notificationProcessingException.getMessage());
return notificationProcessingException;
} finally {
em.merge(sentNotification);
}
return null;
});
if (notificationError != null && error.get() == null) {
error.set(notificationError);
}
}
);
exchange.getMessage().setBody(error.get() == null);
if (error.get() != null) {
throw error.get();
}
})
.onException(Exception.class)
.logStackTrace(false)
.handled(true)
.process(exchange -> {
// Just notify sender in case of RequestReply
exchange.getMessage().setBody(false);
});
}
public boolean sendNotification(Notification notification) {
return sendNotification(notification, INTERNAL, "");
}
public void sendNotificationAsync(Notification notification, Notification.Source source, String sourceId) {
Map headers = Map.ofEntries(
entry(Notification.HEADER_SOURCE, source),
entry(Notification.HEADER_SOURCE_ID, sourceId));
messageBrokerService.getFluentProducerTemplate().withBody(notification).withHeaders(headers).to(NotificationService.NOTIFICATION_QUEUE).send();
}
public boolean sendNotification(Notification notification, Notification.Source source, String sourceId) {
Map headers = Map.ofEntries(
entry(Notification.HEADER_SOURCE, source),
entry(Notification.HEADER_SOURCE_ID, sourceId));
return messageBrokerService.getFluentProducerTemplate().withBody(notification).withHeaders(headers).to(NotificationService.NOTIFICATION_QUEUE).request(Boolean.class);
}
public void setNotificationDelivered(long id) {
setNotificationDelivered(id, timerService.getCurrentTimeMillis());
}
public void setNotificationDelivered(long id, long timestamp) {
persistenceService.doTransaction(entityManager -> {
Query query = entityManager.createQuery("UPDATE SentNotification SET deliveredOn=:timestamp WHERE id =:id");
query.setParameter("id", id);
query.setParameter("timestamp", new Date(timestamp));
query.executeUpdate();
});
}
public void setNotificationAcknowledged(long id, String acknowledgement) {
setNotificationAcknowledged(id, acknowledgement, timerService.getCurrentTimeMillis());
}
public void setNotificationAcknowledged(long id, String acknowledgement, long timestamp) {
persistenceService.doTransaction(entityManager -> {
Query query = entityManager.createQuery("UPDATE SentNotification SET acknowledgedOn=:timestamp, acknowledgement=:acknowledgement WHERE id =:id");
query.setParameter("id", id);
query.setParameter("timestamp", new Date(timestamp));
query.setParameter("acknowledgement", acknowledgement);
query.executeUpdate();
});
}
public SentNotification getSentNotification(Long notificationId) {
return persistenceService.doReturningTransaction(em -> em.find(SentNotification.class, notificationId));
}
public List getNotifications(List ids, List types, Long fromTimestamp, Long toTimestamp, List realmIds, List userIds, List assetIds) throws IllegalArgumentException {
StringBuilder builder = new StringBuilder();
builder.append("select n from SentNotification n where 1=1");
List
© 2015 - 2025 Weber Informatics LLC | Privacy Policy