/*
* Copyright: Almende B.V. (2014), Rotterdam, The Netherlands
* License: The Apache Software License, Version 2.0
*/
package com.almende.eve.agent;
import java.io.IOException;
import java.lang.reflect.Method;
import java.lang.reflect.Type;
import java.net.URI;
import java.util.Iterator;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.joda.time.DateTime;
import com.almende.eve.capabilities.handler.Handler;
import com.almende.eve.capabilities.handler.SimpleHandler;
import com.almende.eve.instantiation.Configurable;
import com.almende.eve.instantiation.HibernationHandler;
import com.almende.eve.instantiation.InstantiationService;
import com.almende.eve.instantiation.InstantiationServiceBuilder;
import com.almende.eve.instantiation.InstantiationServiceConfig;
import com.almende.eve.protocol.Meta;
import com.almende.eve.protocol.Protocol;
import com.almende.eve.protocol.ProtocolBuilder;
import com.almende.eve.protocol.ProtocolConfig;
import com.almende.eve.protocol.ProtocolStack;
import com.almende.eve.protocol.auth.Authorizor;
import com.almende.eve.protocol.jsonrpc.JSONRpcProtocol;
import com.almende.eve.protocol.jsonrpc.JSONRpcProtocolBuilder;
import com.almende.eve.protocol.jsonrpc.JSONRpcProtocolConfig;
import com.almende.eve.protocol.jsonrpc.RpcBasedProtocol;
import com.almende.eve.protocol.jsonrpc.annotation.Access;
import com.almende.eve.protocol.jsonrpc.annotation.AccessType;
import com.almende.eve.protocol.jsonrpc.formats.Caller;
import com.almende.eve.protocol.jsonrpc.formats.JSONMessage;
import com.almende.eve.protocol.jsonrpc.formats.JSONRequest;
import com.almende.eve.scheduling.Scheduler;
import com.almende.eve.scheduling.SchedulerBuilder;
import com.almende.eve.scheduling.SimpleSchedulerConfig;
import com.almende.eve.state.State;
import com.almende.eve.state.StateBuilder;
import com.almende.eve.state.StateConfig;
import com.almende.eve.transport.LocalTransportBuilder;
import com.almende.eve.transport.LocalTransportConfig;
import com.almende.eve.transport.Receiver;
import com.almende.eve.transport.Router;
import com.almende.eve.transport.Transport;
import com.almende.eve.transport.TransportBuilder;
import com.almende.eve.transport.TransportConfig;
import com.almende.util.TypeUtil;
import com.almende.util.callback.AsyncCallback;
import com.almende.util.callback.SyncCallback;
import com.almende.util.jackson.JOM;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
/**
* The Class AgentCore, contains the basic parts of an Eve agent: Single State,
* Single Scheduler, Transports, ProtocolStack and Configuration management.
*/
@Access(AccessType.UNAVAILABLE)
public class AgentCore implements Receiver, Configurable, Authorizor {
private static final Logger LOG = Logger.getLogger(AgentCore.class
.getName());
private String agentId = null;
private ObjectNode literalConfig = null;
private AgentConfig config = null;
private InstantiationService instService = null;
private State state = null;
private Router transport = new Router();
private Scheduler scheduler = null;
private ProtocolStack protocolStack = new ProtocolStack();
private Handler<Receiver> receiver = new SimpleHandler<Receiver>(
this);
private Handler<Object> handler = new SimpleHandler<Object>(
this);
protected Caller caller = new DefaultCaller();
private Handler<Caller> sender = new SimpleHandler<Caller>(
caller);
/**
* Instantiates a new agent.
*/
public AgentCore() {
config = new AgentConfig();
}
/**
* Instantiates a new agent.
*
* @param config
* the config
*/
public AgentCore(ObjectNode config) {
if (config == null) {
config = JOM.createObjectNode();
}
config.put("class", this.getClass().getName());
setConfig(config);
}
/**
* Instantiates a new agent.
*
* @param agentId
* the agent id
* @param config
* the config
*/
public AgentCore(final String agentId, ObjectNode config) {
if (config == null) {
config = JOM.createObjectNode();
}
config.put("id", agentId);
config.put("class", this.getClass().getName());
setConfig(config);
}
/**
* On ready, is being run after the configuration has been loaded.
*/
protected void onReady() {
// Running old methods for backwards compatibility.
onInit();
onBoot();
}
/**
* On destroy, is being run before the destroy() method is started.
*/
protected void onDestroy() {}
/**
* On init.
*
* @deprecated This old event handler is no longer in use, use onReady
* instead.
*/
@Deprecated
protected void onInit() {}
/**
* On boot.
*
* @deprecated This old event handler is no longer in use, use onReady
* instead.
*/
@Deprecated
protected void onBoot() {}
@Override
public boolean onAccess(URI senderUrl, String functionTag) {
return true;
}
@Override
public boolean onAccess(URI senderUrl) {
return true;
}
@Override
public boolean isSelf(URI senderUrl) {
// TODO: check for scheduler URL?
return (caller.getSenderUrls().contains(senderUrl));
}
/**
* Creates a proxy for given URL and interface, with this agent as sender.
*
* @param <T>
* the type represented by the given interface, of which the
* generated proxy is an instance.
* @param url
* the address of the remote agent
* @param agentInterface
* the interface that the remote agent implements
* @return the t
*/
@Access(AccessType.UNAVAILABLE)
protected final <T> T createAgentProxy(final URI url,
final Class<T> agentInterface) {
return AgentProxyFactory.genProxy(this, url, agentInterface);
}
/**
* Destroy the agent.
*
* @param instanceOnly
* the instance only
*/
@Access(AccessType.UNAVAILABLE)
protected void destroy(Boolean instanceOnly) {
onDestroy();
if (scheduler != null) {
scheduler.delete();
scheduler = null;
}
if (transport != null) {
transport.disconnect();
transport.delete();
transport = null;
}
if (protocolStack != null) {
protocolStack.delete();
protocolStack = null;
}
if (state != null) {
state.delete(instanceOnly);
state = null;
}
if (instService != null) {
instService.deregister(agentId);
instService = null;
}
}
/**
* Gets the id.
*
* @return the id
*/
@Access(AccessType.PUBLIC)
public String getId() {
return agentId;
}
/**
* Sets the config.
*
* @param config
* the new config
*/
public void setConfig(final ObjectNode config) {
this.literalConfig = config;
this.config = AgentConfig.decorate(config);
loadConfig();
onReady();
try {
// Asynchronous connect of agent's transports.
connect();
} catch (IOException e) {
LOG.log(Level.WARNING, "Couldn't connect transports", e);
}
}
/**
* Gets the runtime configuration of this agent
*
* @return the config
*/
@Access(AccessType.PUBLIC)
public ObjectNode getConfig() {
return config;
}
/**
* Gets the literal, unextended, unexpanded config, as given during
* initiation.
*
* @return the literal config
*/
@JsonIgnore
@Access(AccessType.PUBLIC)
public ObjectNode getLiteralConfig() {
return literalConfig;
}
private void loadConfig() {
agentId = config.getId();
loadInstantiationService(config.getInstantiationService());
if (instService != null && config.isCanHibernate()) {
setHandler(new HibernationHandler<Object>(this, agentId,
instService));
setReceiver(new HibernationHandler<Receiver>(this, agentId,
instService));
setSender(new HibernationHandler<Caller>(caller, agentId,
instService));
}
loadState(config.getState());
loadProtocols(config.getProtocols());
loadTransports(config.getTransports());
loadScheduler(config.getScheduler());
}
/**
* Override default configuration and set the provided inbound message
* receiver.
* <b>This is meant to extend the agent with new low level behavior, handle
* with care</b>
*
* @param receiver
* the receiver to set
*/
protected void setReceiver(Handler<Receiver> receiver) {
this.receiver = receiver;
}
/**
* Get the current message receiver.
*
* @return the receiver
*/
@JsonIgnore
protected Handler<Receiver> getReceiver() {
return receiver;
}
/**
* Override configuration and set the provided inbound RPC target handler
* <b>This is meant to extend the agent with new low level behavior, handle
* with care</b>
*
* @param handler
* the new RPC target handler
*/
protected void setHandler(Handler<Object> handler) {
this.handler = handler;
}
/**
* Gets the handler.
*
* @return the receiver
*/
@JsonIgnore
public Handler<Object> getHandler() {
return handler;
}
/**
* Connect all transports.
*
* @throws IOException
* Signals that an I/O exception has occurred.
*/
@Access(AccessType.UNAVAILABLE)
public void connect() throws IOException {
transport.connect();
}
/**
* Disconnect all transports.
*/
@Access(AccessType.UNAVAILABLE)
public void disconnect() {
transport.disconnect();
}
/**
* Gets the caller.
*
* @return the caller
*/
@JsonIgnore
protected Caller getCaller() {
return caller;
}
/**
* Override default configuration and set the send handler.
* <b>This is meant to extend the agent with new low level behavior, handle
* with care</b>
*
* @param sender
* the new sender
*/
protected void setSender(Handler<Caller> sender) {
this.sender = sender;
}
/**
* Gets the sender handler.
*
* @return the sender
*/
@JsonIgnore
protected Handler<Caller> getSender() {
return sender;
}
@Access(AccessType.UNAVAILABLE)
@Override
public void receive(final Object msg, final URI senderUrl, final String tag) {
if (protocolStack == null) {
// E.g. during destroy()!
return;
}
protocolStack.inbound(msg, senderUrl, tag);
}
/**
* Gets the instantiationService.
*
* @return the checks if is
*/
@JsonIgnore
protected InstantiationService getInstantiationService() {
return instService;
}
private void loadInstantiationService(final ObjectNode config) {
if (config != null) {
InstantiationServiceConfig iscfg = InstantiationServiceConfig
.decorate(config);
final StateConfig stateConfig = StateConfig.decorate(iscfg
.getState());
if (agentId != null && stateConfig.getId() == null) {
stateConfig.setId(agentId);
iscfg.setState(stateConfig);
}
instService = new InstantiationServiceBuilder().withConfig(iscfg)
.build();
instService.register(agentId, literalConfig, this.getClass()
.getName());
}
}
/**
* Gets the protocol stack.
*
* @return the protocol stack
*/
@JsonIgnore
protected ProtocolStack getProtocolStack() {
return protocolStack;
}
private void loadProtocols(final ArrayNode config) {
boolean found = false;
if (config != null) {
for (JsonNode item : config) {
ProtocolConfig conf = ProtocolConfig
.decorate((ObjectNode) item);
if (agentId != null && conf.getId() == null) {
conf.setId(agentId);
}
final Protocol protocol = new ProtocolBuilder()
.withConfig((ObjectNode) conf).withHandle(handler)
.build();
if (JSONRpcProtocolBuilder.class.getName().equals(
conf.getBuilder())) {
found = true;
}
if (protocol instanceof RpcBasedProtocol) {
final RpcBasedProtocol prot = (RpcBasedProtocol) protocol;
prot.setCaller(sender);
}
protocolStack.add(protocol);
}
}
if (config == null || !found) {
// each agent has at least a JSONRPC protocol handler
final JSONRpcProtocolConfig conf = JSONRpcProtocolConfig.create();
if (agentId != null && conf.getId() == null) {
conf.setId(agentId);
}
final JSONRpcProtocol protocol = new JSONRpcProtocolBuilder()
.withConfig(conf).withHandle(handler).build();
protocol.setCaller(sender);
protocolStack.add(protocol);
}
}
/**
* Schedule an RPC call at a specified due time.
*
* @param request
* the request
* @param due
* the due time
* @return the task id of this scheduled task, for cancellation.
*/
@Access(AccessType.UNAVAILABLE)
protected String schedule(final JSONRequest request, final DateTime due) {
final Scheduler scheduler = this.scheduler;
if (scheduler == null || request == null) {
LOG.warning("Trying to schedule, with missing scheduler/request.");
return "";
}
final JsonNode id = request.getId();
return scheduler.schedule(id != null ? id.toString() : null, request,
due);
}
/**
* Cancel the given scheduled task.
*
* @param taskId
* the task id
*/
@Access(AccessType.UNAVAILABLE)
protected void cancel(final String taskId) {
final Scheduler scheduler = this.scheduler;
if (scheduler == null) {
return;
}
scheduler.cancel(taskId);
}
/**
* Sets the scheduler, updates the agent's configuration in the process.
* <b>Note: this does not update the literal config</b>
*
* @param scheduler
* the new scheduler
*/
@JsonIgnore
protected void setScheduler(final Scheduler scheduler) {
if (this.scheduler != null) {
this.scheduler.clear();
}
this.scheduler = scheduler;
config.set("scheduler", scheduler.getParams());
}
/**
* Gets the scheduler.
*
* @return the scheduler
*/
@JsonIgnore
protected Scheduler getScheduler() {
return scheduler;
}
private void loadScheduler(final ObjectNode params) {
if (params != null) {
final SimpleSchedulerConfig schedulerConfig = SimpleSchedulerConfig
.decorate(params);
if (schedulerConfig != null) {
if (agentId != null && schedulerConfig.has("state")) {
final StateConfig stateConfig = StateConfig
.decorate((ObjectNode) schedulerConfig.get("state"));
if (stateConfig.getId() == null) {
stateConfig.setId("scheduler_" + agentId);
schedulerConfig.set("state", stateConfig);
}
}
if (agentId != null && schedulerConfig.getId() == null) {
schedulerConfig.setId(agentId);
}
scheduler = new SchedulerBuilder().withConfig(schedulerConfig)
.withHandle(sender).build();
}
}
}
/**
* Sets the state, updates the agent's runtime configuration in the process.
* <b>Note: this does not update the literal config</b>
*
* @param state
* the new state
*/
@JsonIgnore
protected void setState(final State state) {
this.state = state;
config.set("state", state.getParams());
}
/**
* Gets the state.
*
* @return the state
*/
@Access(AccessType.UNAVAILABLE)
@JsonIgnore
protected State getState() {
return state;
}
private void loadState(final ObjectNode sc) {
if (sc != null) {
final StateConfig stateConfig = StateConfig.decorate(sc);
if (agentId != null && stateConfig.getId() == null) {
stateConfig.setId(agentId);
}
state = new StateBuilder().withConfig(stateConfig).build();
}
}
/**
* Override configuration and set the transport router
* <b>Caution: this will replace the router, which will drop all earlier
* configured outbound transports! This should be called before any
* transports are set and all transports should be disconnected.</b>
*
* @param transport
* the new transport router
*/
protected void setTransport(Router transport) {
this.transport = transport;
}
/**
* Adds a new transport to this router/array, updates the agent's
* configuration in the process.
* <b>Note: this does not update the literal config</b>
*
* @param transport
* the transport router
*/
protected void addTransport(final Transport transport) {
this.transport.register(transport);
this.config.addTransport(transport.getParams());
}
/**
* Gets the transport router.
*
* @return the transport
*/
@JsonIgnore
protected Router getTransport() {
return transport;
}
private void addTransport(final ObjectNode transconfig) {
// TODO: Somewhat ugly, not every transport requires an id.
TransportConfig transconf = TransportConfig.decorate(transconfig);
if (transconf.get("id") == null) {
transconf.put("id", agentId);
}
final Transport transport = new TransportBuilder()
.withConfig(transconf).withHandle(receiver).build();
this.transport.register(transport);
}
private void loadTransports(final ArrayNode transportConfig) {
if (transportConfig != null) {
final Iterator<JsonNode> iter = transportConfig.iterator();
while (iter.hasNext()) {
addTransport((ObjectNode) iter.next());
}
}
// All agents have a local transport
this.transport.register(new LocalTransportBuilder()
.withConfig(LocalTransportConfig.create(agentId))
.withHandle(receiver).build());
}
private class DefaultCaller implements Caller {
@Override
public void call(final URI url, final Object message)
throws IOException {
final Meta wrapper = protocolStack.outbound(message, url, null);
if (wrapper != null) {
transport.send(wrapper.getPeer(), wrapper.getMsg(),
wrapper.getTag(), null);
}
}
@Override
public <T> void call(final URI url, final JSONMessage message,
final String tag) throws IOException {
final Meta wrapper = protocolStack.outbound(message, url, tag);
if (wrapper != null) {
transport.send(wrapper.getPeer(), wrapper.getMsg(),
wrapper.getTag(), null);
}
}
@Override
public void call(final URI url, final JSONMessage message)
throws IOException {
call(url, message, null);
}
@Override
public <T> void call(final URI url, final String method,
final ObjectNode params, final AsyncCallback<T> callback)
throws IOException {
final JSONRequest message = new JSONRequest(method, params,
callback);
call(url, message, null);
}
@Override
public <T> void call(final URI url, final Method method,
final Object[] params, final AsyncCallback<T> callback)
throws IOException {
final JSONRequest message = new JSONRequest(method, params,
callback);
final Meta wrapper = protocolStack.outbound(message, url, null);
if (wrapper != null) {
transport.send(wrapper.getPeer(), wrapper.getMsg(),
wrapper.getTag(), callback);
}
}
@Override
public void call(final URI url, final String method,
final ObjectNode params) throws IOException {
call(url, method, params, null);
}
@Override
public void call(final URI url, final Method method,
final Object[] params) throws IOException {
call(url, method, params, null);
}
@Override
public <T> T callSync(final URI url, final String method,
final ObjectNode params, final Class<T> clazz)
throws IOException {
return (T) callSync(url, method, params, TypeUtil.get(clazz));
}
@SuppressWarnings("unchecked")
@Override
public <T> T callSync(final URI url, final String method,
final ObjectNode params, final JavaType type)
throws IOException {
return (T) callSync(url, method, params, TypeUtil.get(type));
}
@SuppressWarnings("unchecked")
@Override
public <T> T callSync(final URI url, final String method,
final ObjectNode params, final Type type) throws IOException {
return (T) callSync(url, method, params, TypeUtil.get(type));
}
@Override
public <T> T callSync(final URI url, final String method,
final ObjectNode params, final TypeUtil<T> type)
throws IOException {
final SyncCallback<T> callback = new SyncCallback<T>(type) {};
final JSONRequest message = new JSONRequest(method, params,
callback);
final Meta wrapper = protocolStack.outbound(message, url, null);
if (wrapper != null) {
transport.send(wrapper.getPeer(), wrapper.getMsg(),
wrapper.getTag(), callback);
}
try {
return callback.get();
} catch (final Exception e) {
throw new IOException(e);
}
}
@Override
public List<URI> getSenderUrls() {
return transport.getAddresses();
}
@Override
public URI getSenderUrlByScheme(final String scheme) {
return transport.getAddressByScheme(scheme);
}
}
}