package org.zstack.core.cloudbus;
import org.springframework.beans.factory.annotation.Autowired;
import org.zstack.core.Platform;
import org.zstack.core.db.Q;
import org.zstack.core.thread.AsyncThread;
import org.zstack.core.webhook.WebhookCaller;
import org.zstack.header.core.webhooks.WebhookVO_;
import org.zstack.header.Component;
import org.zstack.header.apimediator.ApiMessageInterceptionException;
import org.zstack.header.apimediator.GlobalApiMessageInterceptor;
import org.zstack.header.core.webhooks.APICreateWebhookMsg;
import org.zstack.header.core.webhooks.WebhookInventory;
import org.zstack.header.core.webhooks.WebhookVO;
import org.zstack.header.message.APIMessage;
import org.zstack.header.message.Event;
import org.zstack.utils.gson.JSONObjectUtil;
import static org.zstack.core.Platform.argerr;
import java.util.*;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.stream.Collectors;
import static java.util.Arrays.asList;
/**
* Created with IntelliJ IDEA.
* User: frank
* Time: 11:39 PM
* To change this template use File | Settings | File Templates.
*/
public class EventFacadeImpl implements EventFacade, CloudBusEventListener, Component, GlobalApiMessageInterceptor {
@Autowired
private CloudBus bus;
private final Map<String, CallbackWrapper> global = Collections.synchronizedMap(new HashMap<>());
private final Map<String, CallbackWrapper> local = Collections.synchronizedMap(new HashMap<>());
private EventSubscriberReceipt unsubscriber;
@Override
public List<Class> getMessageClassToIntercept() {
return asList(APICreateWebhookMsg.class);
}
@Override
public InterceptorPosition getPosition() {
return InterceptorPosition.FRONT;
}
@Override
public APIMessage intercept(APIMessage msg) throws ApiMessageInterceptionException {
if (msg instanceof APICreateWebhookMsg) {
validate((APICreateWebhookMsg) msg);
}
return msg;
}
private void validate(APICreateWebhookMsg msg) {
if (!EventFacade.WEBHOOK_TYPE.equals(msg.getType())) {
return;
}
if (msg.getOpaque() == null) {
throw new ApiMessageInterceptionException(argerr("for webhooks with type[%s], the field opaque cannot be null", EventFacade.WEBHOOK_TYPE));
}
}
private class CallbackWrapper {
String path;
String glob;
AbstractEventFacadeCallback callback;
AtomicBoolean hasRun;
CallbackWrapper(String path, AbstractEventFacadeCallback callback) {
this.path = path;
this.glob = createRegexFromGlob(path.replaceAll("\\{.*\\}", ".*"));
this.callback = callback;
if (callback instanceof AutoOffEventCallback) {
hasRun = new AtomicBoolean(false);
}
}
Object getIdentity() {
return callback;
}
public String getGlob() {
return glob;
}
@AsyncThread
void call(CanonicalEvent e) {
if (callback instanceof EventRunnable) {
((EventRunnable) callback).run();
} else {
Map<String, String> tokens = tokenize(e.getPath(), path);
tokens.put(EventFacade.META_DATA_MANAGEMENT_NODE_ID, e.getManagementNodeId());
tokens.put(EventFacade.META_DATA_PATH, e.getPath());
Object data = null;
if (e.getContent() != null) {
data = e.getContent();
}
if (callback instanceof EventCallback) {
((EventCallback)callback).run(tokens, data);
} else if (callback instanceof AutoOffEventCallback) {
if (!hasRun.compareAndSet(false, true)) {
// the callback is being called
return;
}
if (((AutoOffEventCallback)callback).run(tokens, data)) {
off(callback);
} else {
hasRun.set(false);
}
}
}
}
}
public String createRegexFromGlob(String glob) {
String out = "^";
for(int i = 0; i < glob.length(); ++i) {
final char c = glob.charAt(i);
switch(c) {
case '*': out += ".*"; break;
case '?': out += '.'; break;
case '\\': out += "\\\\"; break;
default: out += c;
}
}
out += '$';
return out;
}
private Map<String, String> tokenize(String str1, String str2) {
StringTokenizer token = new StringTokenizer(str1, "/");
List<String> origins = new ArrayList<String>();
while (token.hasMoreElements()) {
origins.add(token.nextToken());
}
token = new StringTokenizer(str2, "/");
List<String> t = new ArrayList<String>();
while (token.hasMoreElements()) {
t.add(token.nextToken());
}
Map ret = new HashMap();
for (int i=0;i<t.size(); i++) {
String key = t.get(i);
if (!key.startsWith("{") || !key.endsWith("}")) {
continue;
}
key = key.replaceAll("\\{", "").replaceAll("\\}", "");
ret.put(key, origins.get(i));
}
return ret;
}
@Override
public void on(String path, AutoOffEventCallback cb) {
global.put(cb.uniqueIdentity, new CallbackWrapper(path, cb));
}
@Override
public void on(String path, final EventCallback cb) {
global.put(cb.uniqueIdentity, new CallbackWrapper(path, cb));
}
@Override
public void on(String path, EventRunnable cb) {
global.put(cb.uniqueIdentity, new CallbackWrapper(path, cb));
}
@Override
public void off(AbstractEventFacadeCallback cb) {
global.remove(cb.uniqueIdentity);
local.remove(cb.uniqueIdentity);
}
@Override
public void onLocal(String path, AutoOffEventCallback cb) {
local.put(cb.uniqueIdentity, new CallbackWrapper(path, cb));
}
@Override
public void onLocal(String path, EventCallback cb) {
local.put(cb.uniqueIdentity, new CallbackWrapper(path, cb));
}
@Override
public void onLocal(String path, EventRunnable cb) {
local.put(cb.uniqueIdentity, new CallbackWrapper(path, cb));
}
@Override
public void fire(String path, Object data) {
assert path != null;
CanonicalEvent evt = new CanonicalEvent();
evt.setPath(path);
evt.setManagementNodeId(Platform.getManagementServerId());
if (data != null) {
/*
if (!TypeUtils.isPrimitiveOrWrapper(data.getClass()) && !data.getClass().isAnnotationPresent(NeedJsonSchema.class)) {
throw new CloudRuntimeException(String.format("data[%s] passed to canonical event is not annotated by @NeedJsonSchema", data.getClass().getName()));
}
*/
evt.setContent(data);
}
fireLocal(evt);
callWebhooks(evt);
bus.publish(evt);
}
private void callWebhooks(CanonicalEvent event) {
new WebhookCaller() {
@Override
public void call() {
List<WebhookVO> vos = Q.New(WebhookVO.class).eq(WebhookVO_.type, EventFacade.WEBHOOK_TYPE).list();
vos = vos.stream().filter(
vo -> event.getPath().matches(
createRegexFromGlob(vo.getOpaque().replaceAll("\\{.*\\}", ".*"))
)).collect(Collectors.toList());
if (!vos.isEmpty()) {
postToWebhooks(WebhookInventory.valueOf(vos), JSONObjectUtil.toJsonString(event));
}
}
}.call();
}
private void fireLocal(CanonicalEvent cevt) {
Map<String, CallbackWrapper> wrappers = new HashMap<>();
wrappers.putAll(local);
for (CallbackWrapper w : wrappers.values()) {
if (cevt.getPath().matches(w.getGlob())) {
w.call(cevt);
}
}
}
@Override
public boolean isFromThisManagementNode(Map tokens) {
return Platform.getManagementServerId().equals(tokens.get(EventFacade.META_DATA_MANAGEMENT_NODE_ID));
}
@Override
public boolean handleEvent(Event evt) {
if (!(evt instanceof CanonicalEvent)) {
return false;
}
CanonicalEvent cevt = (CanonicalEvent)evt;
Map<String, CallbackWrapper> wrappers = new HashMap<>();
wrappers.putAll(global);
for (CallbackWrapper w : wrappers.values()) {
if (cevt.getPath().matches(w.getGlob())) {
w.call(cevt);
}
}
return false;
}
@Override
public boolean start() {
unsubscriber = bus.subscribeEvent(this, new CanonicalEvent());
return true;
}
@Override
public boolean stop() {
if (unsubscriber != null) {
unsubscriber.unsubscribeAll();
}
return true;
}
}