package org.zstack.core.notification;
import org.apache.commons.validator.routines.UrlValidator;
import org.springframework.beans.factory.annotation.Autowired;
import org.zstack.core.Platform;
import org.zstack.core.cloudbus.CloudBus;
import org.zstack.core.cloudbus.MessageSafe;
import org.zstack.core.componentloader.PluginRegistry;
import org.zstack.core.config.GlobalConfigException;
import org.zstack.core.config.GlobalConfigFacade;
import org.zstack.core.config.GlobalConfigValidatorExtensionPoint;
import org.zstack.core.db.DatabaseFacade;
import org.zstack.core.db.SQL;
import org.zstack.core.db.SQLBatch;
import org.zstack.core.db.SQLBatchWithReturn;
import org.zstack.core.thread.AsyncThread;
import org.zstack.core.thread.Task;
import org.zstack.core.thread.ThreadFacade;
import org.zstack.header.AbstractService;
import org.zstack.header.core.ExceptionSafe;
import org.zstack.header.errorcode.OperationFailureException;
import org.zstack.header.message.*;
import org.zstack.header.notification.ApiNotification;
import org.zstack.header.notification.ApiNotificationFactory;
import org.zstack.header.notification.ApiNotificationFactoryExtensionPoint;
import org.zstack.header.rest.RESTFacade;
import org.zstack.utils.Utils;
import org.zstack.utils.gson.JSONObjectUtil;
import org.zstack.utils.logging.CLogger;
import static org.zstack.core.Platform.argerr;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.*;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;
/**
* Created by xing5 on 2017/3/15.
*/
public class NotificationManager extends AbstractService {
private CLogger logger = Utils.getLogger(getClass());
@Autowired
private CloudBus bus;
@Autowired
private DatabaseFacade dbf;
@Autowired
private PluginRegistry plugRgty;
@Autowired
private ThreadFacade thdf;
@Autowired
private RESTFacade restf;
private Map<Class, ApiNotificationFactory> apiNotificationFactories = new HashMap<>();
private BlockingQueue<NotificationBuilder> notificationsQueue = new LinkedBlockingQueue<>();
private NotificationBuilder quitToken = new NotificationBuilder();
private boolean exitQueue = false;
private class ApiNotificationSender implements BeforeDeliveryMessageInterceptor, BeforePublishEventInterceptor {
class Bundle {
APIMessage message;
ApiNotification notification;
}
ConcurrentHashMap<String, Bundle> apiMessages = new ConcurrentHashMap<>();
Map<Class, Method> notificationMethods = new ConcurrentHashMap<>();
public ApiNotificationSender() {
Set<Method> methods = Platform.getReflections().getMethodsReturn(ApiNotification.class);
for (Method m : methods) {
notificationMethods.put(m.getDeclaringClass(), m);
}
}
@Override
public int orderOfBeforePublishEventInterceptor() {
return 0;
}
private ApiNotification getApiNotification(APIMessage msg) throws InvocationTargetException, IllegalAccessException {
Method m = notificationMethods.get(msg.getClass());
if (m == null) {
Class clz = msg.getClass().getSuperclass();
while (clz != Object.class) {
m = notificationMethods.get(clz);
if (m != null) {
break;
}
clz = clz.getSuperclass();
}
if (m != null) {
notificationMethods.put(msg.getClass(), m);
}
}
ApiNotification notification = null;
if (m != null) {
notification = (ApiNotification) m.invoke(msg);
} else {
ApiNotificationFactory factory = apiNotificationFactories.get(msg.getClass());
if (factory != null) {
notification = factory.createApiNotification(msg);
}
}
return notification;
}
@Override
@AsyncThread
public void beforePublishEvent(Event evt) {
if (!(evt instanceof APIEvent)) {
return;
}
APIEvent aevt = (APIEvent) evt;
Bundle b = apiMessages.get(aevt.getApiId());
if (b == null) {
return;
}
apiMessages.remove(aevt.getApiId());
b.notification.after((APIEvent) evt);
List<NotificationBuilder> lst = new ArrayList<>();
for (ApiNotification.Inner inner : b.notification.getInners()) {
Map opaque = new HashMap();
opaque.put("session", b.message.getSession());
opaque.put("success", aevt.isSuccess());
if (!aevt.isSuccess()) {
opaque.put("error", aevt.getError());
}
lst.add(new NotificationBuilder()
.content(inner.getContent())
.arguments(inner.getArguments())
.name(NotificationConstant.API_SENDER)
.sender(NotificationConstant.API_SENDER)
.resource(inner.getResourceUuid(), inner.getResourceType())
.opaque(opaque));
}
send(lst);
}
@Override
public int orderOfBeforeDeliveryMessageInterceptor() {
return 0;
}
@Override
public void intercept(Message msg) {
if (!(msg instanceof APIMessage)) {
return;
}
if (msg instanceof APISyncCallMessage) {
return;
}
if (!msg.getServiceId().endsWith(Platform.getManagementServerId())) {
// a message to api portal
return;
}
try {
ApiNotification notification = getApiNotification((APIMessage) msg);
if (notification == null) {
logger.warn(String.format("API message[%s] does not have an API notification method or the method returns null",
msg.getClass()));
return;
}
notification.before();
Bundle b = new Bundle();
b.message = (APIMessage) msg;
b.notification = notification;
apiMessages.put(msg.getId(), b);
} catch (Throwable t) {
logger.warn(String.format("unhandled exception %s", t.getMessage()), t);
}
}
}
private ApiNotificationSender apiNotificationSender = new ApiNotificationSender();
void send(List<NotificationBuilder> builders) {
for (NotificationBuilder builder : builders) {
send(builder);
}
}
void send(NotificationBuilder builder) {
try {
notificationsQueue.offer(builder, 60, TimeUnit.SECONDS);
} catch (InterruptedException e) {
logger.warn(String.format("unable to write log %s", JSONObjectUtil.toJsonString(builder)), e);
}
}
@Override
public boolean start() {
bus.installBeforeDeliveryMessageInterceptor(apiNotificationSender);
bus.installBeforePublishEventInterceptor(apiNotificationSender);
for (ApiNotificationFactoryExtensionPoint ext : plugRgty.getExtensionList(ApiNotificationFactoryExtensionPoint.class)) {
apiNotificationFactories.putAll(ext.apiNotificationFactory());
}
NotificationGlobalConfig.WEBHOOK_URL.installValidateExtension(new GlobalConfigValidatorExtensionPoint() {
@Override
public void validateGlobalConfig(String category, String name, String oldValue, String newValue) throws GlobalConfigException {
if (newValue == null || "null".equals(newValue)) {
return;
}
if (!new UrlValidator().isValid(newValue)) {
throw new OperationFailureException(argerr("%s is not a valid URL", newValue));
}
}
});
thdf.submit(new Task<Void>() {
@Override
public Void call() throws Exception {
writeNotificationsToDb();
return null;
}
@Override
public String getName() {
return "notification-thread";
}
});
return true;
}
@ExceptionSafe
private void writeNotificationsToDb() throws InterruptedException {
while (!exitQueue) {
List<NotificationBuilder> lst = new ArrayList<>();
lst.add(notificationsQueue.take());
notificationsQueue.drainTo(lst);
try {
List<NotificationInventory> invs = new SQLBatchWithReturn<List<NotificationInventory>>() {
@Override
protected List<NotificationInventory> scripts() {
List<NotificationInventory> invs = new ArrayList<>();
for (NotificationBuilder builder : lst) {
if (builder == quitToken) {
exitQueue = true;
continue;
}
NotificationVO vo = new NotificationVO();
vo.setName(builder.notificationName);
vo.setArguments(JSONObjectUtil.toJsonString(builder.arguments));
vo.setContent(builder.content);
vo.setResourceType(builder.resourceType);
vo.setResourceUuid(builder.resourceUuid);
vo.setSender(builder.sender);
vo.setStatus(NotificationStatus.Unread);
vo.setType(builder.type);
vo.setUuid(Platform.getUuid());
vo.setTime(System.currentTimeMillis());
if (builder.opaque != null) {
vo.setOpaque(JSONObjectUtil.toJsonString(builder.opaque));
}
dbf.getEntityManager().persist(vo);
invs.add(NotificationInventory.valueOf(vo));
}
return invs;
}
}.execute();
if (NotificationGlobalConfig.WEBHOOK_URL.value() != null && !NotificationGlobalConfig.WEBHOOK_URL.value().equals("null")) {
callWebhook(invs);
}
} catch (Throwable t) {
logger.warn(String.format("failed to persists notifications:\n %s", JSONObjectUtil.toJsonString(lst)), t);
}
}
}
@AsyncThread
private void callWebhook(List<NotificationInventory> lst) {
restf.getRESTTemplate().postForEntity(NotificationGlobalConfig.WEBHOOK_URL.value(), JSONObjectUtil.toJsonString(lst), String.class);
}
@Override
public boolean stop() {
notificationsQueue.offer(quitToken);
return true;
}
@Override
@MessageSafe
public void handleMessage(Message msg) {
if (msg instanceof APIMessage) {
handleApiMessage((APIMessage) msg);
} else {
bus.dealWithUnknownMessage(msg);
}
}
private void handleApiMessage(APIMessage msg) {
if (msg instanceof APIUpdateNotificationsStatusMsg) {
handle((APIUpdateNotificationsStatusMsg) msg);
} else if (msg instanceof APIDeleteNotificationsMsg) {
handle((APIDeleteNotificationsMsg) msg);
} else {
bus.dealWithUnknownMessage(msg);
}
}
private void handle(APIDeleteNotificationsMsg msg) {
APIDeleteNotificationsEvent evt = new APIDeleteNotificationsEvent(msg.getId());
SQL.New(NotificationVO.class).in(NotificationVO_.uuid, msg.getUuids()).delete();
bus.publish(evt);
}
private void handle(APIUpdateNotificationsStatusMsg msg) {
APIUpdateNotificationsStatusEvent evt = new APIUpdateNotificationsStatusEvent(msg.getId());
SQL.New(NotificationVO.class).set(NotificationVO_.status, NotificationStatus.valueOf(msg.getStatus()))
.in(NotificationVO_.uuid, msg.getUuids()).update();
bus.publish(evt);
}
@Override
public String getId() {
return bus.makeLocalServiceId(NotificationConstant.SERVICE_ID);
}
}