/*
* Copyright 2012 Research Studios Austria Forschungsges.m.b.H.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package won.bot.framework.eventbot.listener.baStateBots;
import org.apache.jena.query.*;
import org.apache.jena.rdf.model.Model;
import org.apache.jena.rdf.model.RDFNode;
import org.apache.jena.rdf.model.impl.ResourceImpl;
import won.bot.framework.eventbot.EventListenerContext;
import won.bot.framework.eventbot.event.*;
import won.bot.framework.eventbot.event.impl.wonmessage.ConnectFromOtherNeedEvent;
import won.bot.framework.eventbot.filter.EventFilter;
import won.bot.framework.eventbot.filter.impl.*;
import won.bot.framework.eventbot.listener.AbstractFinishingListener;
import won.protocol.exception.WonMessageBuilderException;
import won.protocol.message.WonMessage;
import won.protocol.message.WonMessageBuilder;
import won.protocol.service.WonNodeInformationService;
import won.protocol.util.RdfUtils;
import won.protocol.util.WonRdfUtils;
import won.protocol.util.linkeddata.CachingLinkedDataSource;
import won.protocol.util.linkeddata.LinkedDataSource;
import won.protocol.util.linkeddata.WonLinkedDataUtils;
import java.net.URI;
import java.text.MessageFormat;
import java.util.Date;
/**
* Listener used to execute a business activity test script. It knows the URIs of one participant and one
* coordinator and sends messages on behalf of these two as defined by the script it is given.
*
* It expects other listeners to send connect messages for the needs it controls.
*/
public class BATestScriptListener extends AbstractFinishingListener
{
private BATestBotScript script;
private URI coordinatorURI;
private URI participantURI;
private URI coordinatorSideConnectionURI = null;
private URI participantSideConnectionURI = null;
private int messagesInFlight = 0;
private final Object countMonitor = new Object();
private final Object filterChangeMonitor = new Object();
private long millisBetweenMessages = 10;
public BATestScriptListener(final EventListenerContext context, final BATestBotScript script,
final URI coordinatorURI, final URI participantURI, long millisBetweenMessages) {
super(context, createEventFilter(coordinatorURI, participantURI));
this.script = script;
this.coordinatorURI = coordinatorURI;
this.participantURI = participantURI;
this.millisBetweenMessages = millisBetweenMessages;
this.name=script.getName();
}
public BATestScriptListener(final EventListenerContext context, final String name, final BATestBotScript script,
final URI coordinatorURI, final URI participantURI, long millisBetweenMessages) {
super(context, name, createEventFilter(coordinatorURI, participantURI));
this.script = script;
this.coordinatorURI = coordinatorURI;
this.participantURI = participantURI;
this.millisBetweenMessages = millisBetweenMessages;
}
protected static EventFilter createEventFilter(URI coordinatorURI, URI participantURI) {
AndFilter mainFilter = new AndFilter();
OrFilter orFilter = new OrFilter();
orFilter.addFilter(new NeedUriEventFilter(coordinatorURI));
orFilter.addFilter(new NeedUriEventFilter(participantURI));
mainFilter.addFilter(orFilter);
return mainFilter;
}
@Override
public boolean isFinished() {
synchronized (countMonitor) {
//messagesInFlight may become negative if a message is received that we didn't send.
boolean bFinished =(!script.hasNext()) && messagesInFlight <= 0;
logger.debug("isFinished()=={}, scripts.hasNext()=={}, messagesInFlight =={}", new Object[]{bFinished,
script.hasNext(),
messagesInFlight});
return bFinished;
}
}
@Override
protected void unsubscribe() {
getEventListenerContext().getEventBus().unsubscribe(this);
}
@Override
protected void handleEvent(final Event event) throws Exception {
if (!(event instanceof NeedSpecificEvent && event instanceof ConnectionSpecificEvent)) {
return;
}
logger.debug("handling event: {}", event);
//extract need URI and connection URI from event
URI needURI = ((NeedSpecificEvent) event).getNeedURI();
URI connectionURI = ((ConnectionSpecificEvent) event).getConnectionURI();
//at the beginning, we don't know the connection URIs - they are generated by the connect call
//so we have to check if the event we're seeing is really about the connection we're interested in
//it could be about another connection of one of the two needs.
synchronized (filterChangeMonitor) {
if (!bothConnectionURIsAreKnown()) {
if (! isConnectionURIKnown(connectionURI)) {
//we haven't checked the connectionURI before
//we have to check if the connectionURI is relevant
if (isRelevantEvent(event, needURI, connectionURI)) {
//the connectionURI is relevant. remember the connect
rememberConnectionURI(needURI, connectionURI);
if (bothConnectionURIsAreKnown()){
updateFilterForBothConnectionURIs();
}
} else {
addIrrelevantConnectionURIToFilter(connectionURI);
logger.debug("omitting event {} as it is not relevant for listener {}", event, this);
return;
}
}
} else if (!isConnectionURIKnown(connectionURI)){
logger.debug("omitting event {} as it is not relevant for listener {}", event, this);
return;
}
}
if (event instanceof ConnectFromOtherNeedEvent){
//send an automatic open
logger.debug("sending automatic open in response to connect");
sendOpen(connectionURI, new Date(System.currentTimeMillis() + millisBetweenMessages));
synchronized (countMonitor){
this.messagesInFlight++;
}
return;
}
// execute the next script action
if (this.script.hasNext()){
//if there is an action, execute it.
BATestScriptAction action = this.script.getNextAction();
logger.debug("executing next script action: {}", action);
if (action.isNopAction()){
logger.debug("not sending any messages for action {}", action);
//if there are no more messages in the script, we're done:
// it means we don't expect to receive more messages from anyone else, and
// because we don't send one, we won't receive one from ourselves
if (!script.hasNext()){
logger.debug("unsubscribing from all events as last script action is NOP");
performFinish();
}
} else {
URI fromCon = getConnectionToSendFrom(action.isSenderIsCoordinator());
URI fromNeed = getNeedToSendFrom(action.isSenderIsCoordinator());
URI toCon = getConnectionToSendFrom(!action.isSenderIsCoordinator());
URI toNeed = getNeedToSendFrom(!action.isSenderIsCoordinator());
logger.debug("sending message for action {} on connection {}", action, fromCon);
assertCorrectConnectionState(fromCon, action);
sendMessage(action, fromCon, fromNeed, toCon, toNeed, new Date(System.currentTimeMillis() +
millisBetweenMessages));
synchronized (countMonitor){
this.messagesInFlight++;
}
}
} else {
logger.debug("script has no more actions.");
}
//in any case: remember that we processed a message. Especially important for the message sent
//through the last action, which we have to process as well otherwise the listener will finish too early
//which may cause the bot to finish and the whole application to shut down before all messages have been
//received, which leads to ugly exceptions
if (event instanceof MessageEvent){
//only decrement the message counter if the event indicates that
//we received a message
synchronized (countMonitor){
this.messagesInFlight--;
}
}
}
private void assertCorrectConnectionState(final URI fromCon, final BATestScriptAction action) {
LinkedDataSource linkedDataSource = getEventListenerContext().getLinkedDataSource();
if (linkedDataSource instanceof CachingLinkedDataSource) {
((CachingLinkedDataSource)linkedDataSource).invalidate(fromCon);
}
logger.debug("fromCon {}, stateOfSenderBeforeSending{}", fromCon, action.getStateOfSenderBeforeSending());
Dataset dataModel = linkedDataSource.getDataForResource(fromCon);
logger.debug("crawled dataset for fromCon {}: {}", fromCon, RdfUtils.toString(dataModel));
String sparqlPrefix =
"PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>"+
"PREFIX geo: <http://www.w3.org/2003/01/geo/wgs84_pos#>"+
"PREFIX xsd: <http://www.w3.org/2001/XMLSchema#>"+
"PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>"+
"PREFIX won: <http://purl.org/webofneeds/model#>"+
"PREFIX wontx: <http://purl.org/webofneeds/tx/model#>"+
"PREFIX gr: <http://purl.org/goodrelations/v1#>"+
"PREFIX sioc: <http://rdfs.org/sioc/ns#>"+
"PREFIX ldp: <http://www.w3.org/ns/ldp#>";
String queryString = sparqlPrefix +
"ASK WHERE { ?con wontx:hasBAState ?state }";
QuerySolutionMap binding = new QuerySolutionMap();
binding.add("con", new ResourceImpl(fromCon.toString()));
binding.add("state", new ResourceImpl(action.getStateOfSenderBeforeSending().toString()));
Query query = QueryFactory.create(queryString);
QueryExecution qExec = QueryExecutionFactory.create(query, dataModel, binding);
boolean result = qExec.execAsk();
//check if the connection is really in the state required for the action
if (result) return;
//we detected an error. Throw an exception.
//query again, this time fetch the state so we can display an informaitive error message
queryString = sparqlPrefix +
"SELECT ?state WHERE { ?con wontx:hasBAState ?state }";
binding = new QuerySolutionMap();
binding.add("con", new ResourceImpl(fromCon.toString()));
query = QueryFactory.create(queryString);
qExec = QueryExecutionFactory.create(query, dataModel, binding);
ResultSet res = qExec.execSelect();
if (! res.hasNext()) {
throw new IllegalStateException("connection state of connection " + fromCon +" does " +
"not allow next action " + action +". Could not determine actual connection state: not found");
}
QuerySolution solution = res.next();
RDFNode state = solution.get("state");
throw new IllegalStateException("connection state " + state + " of connection " + fromCon +" does " +
"not allow next action " + action);
}
private boolean bothConnectionURIsAreKnown() {
return this.participantSideConnectionURI != null && this.coordinatorSideConnectionURI != null;
}
private void sendMessage(final BATestScriptAction action, final URI fromCon, final URI fromNeed, final URI toCon,
final URI toNeed, Date when) throws
Exception {
assert action != null : "action must not be null";
assert fromCon != null : "fromCon must not be null";
assert when != null : "when must not be null";
logger.debug("scheduling connection message for date {}", when);
getEventListenerContext().getTaskScheduler().schedule(new Runnable()
{
public void run() {
try {
getEventListenerContext().getWonMessageSender().sendWonMessage(createWonMessageForConnectionMessage(
fromCon, fromNeed, toCon, toNeed,
action.getMessageToBeSent()));
} catch (Exception e) {
logger.warn("could not send message from {} ", fromCon);
logger.warn("caught exception", e);
}
}
}, when);
}
private WonMessage createWonMessageForConnectionMessage(URI fromConUri, URI fromNeedUri, URI toConUri, URI toNeedUri,
Model content)
throws WonMessageBuilderException {
WonNodeInformationService wonNodeInformationService =
getEventListenerContext().getWonNodeInformationService();
Dataset localNeedRDF =
getEventListenerContext().getLinkedDataSource().getDataForResource(fromNeedUri);
Dataset remoteNeedRDF =
getEventListenerContext().getLinkedDataSource().getDataForResource(toNeedUri);
URI localWonNode = WonRdfUtils.NeedUtils.getWonNodeURIFromNeed(localNeedRDF, fromNeedUri);
URI remoteWonNode = WonRdfUtils.NeedUtils.getWonNodeURIFromNeed(remoteNeedRDF, toNeedUri);
return WonMessageBuilder
.setMessagePropertiesForConnectionMessage(
wonNodeInformationService.generateEventURI(
localWonNode),
fromConUri,
fromNeedUri,
localWonNode,
toConUri,
toNeedUri,
remoteWonNode,
content)
.build();
}
private WonMessage createWonMessageForOpen(URI fromConUri, URI fromNeedUri, URI toConUri, URI toNeedUri)
throws WonMessageBuilderException {
WonNodeInformationService wonNodeInformationService =
getEventListenerContext().getWonNodeInformationService();
Dataset localNeedRDF =
getEventListenerContext().getLinkedDataSource().getDataForResource(fromNeedUri);
Dataset remoteNeedRDF =
getEventListenerContext().getLinkedDataSource().getDataForResource(toNeedUri);
URI localWonNode = WonRdfUtils.NeedUtils.getWonNodeURIFromNeed(localNeedRDF, fromNeedUri);
URI remoteWonNode = WonRdfUtils.NeedUtils.getWonNodeURIFromNeed(remoteNeedRDF, toNeedUri);
return WonMessageBuilder
.setMessagePropertiesForOpen(
wonNodeInformationService.generateEventURI(
localWonNode),
fromConUri,
fromNeedUri,
localWonNode,
toConUri,
toNeedUri,
remoteWonNode,null)
.build();
}
private void sendOpen(final URI connectionURI, Date when) throws Exception {
assert connectionURI != null : "connectionURI must not be null";
assert when != null : "when must not be null";
logger.debug("scheduling connection message for date {}",when);
getEventListenerContext().getTaskScheduler().schedule(new Runnable()
{
public void run()
{
try {
//TODO: THIS STILL HAS TO BE ADAPTED TO NEW MESSAGE FORMAT!
//getEventListenerContext().getWonMessageSender().sendWonMessage(connectionURI, null, null);
throw new UnsupportedOperationException("Not yet adapted to new message format!");
} catch (Exception e) {
logger.warn("could not send open from {} ", connectionURI);
logger.warn("caught exception", e);
}
}
}, when);
}
private URI getConnectionToSendFrom(final boolean senderIsCoordinator) {
return senderIsCoordinator ? coordinatorSideConnectionURI : participantSideConnectionURI;
}
private URI getNeedToSendFrom(final boolean senderIsCoordinator) {
return senderIsCoordinator ? coordinatorURI : participantURI;
}
/**
* Checks whether the event is really about the connection between coordinator and participant by
* fetching the linked data description for the connection and checking the remoteNeed URI.
*/
private boolean isRelevantEvent(final Event event, final URI needURI, final URI connectionURI) {
URI remoteNeedURI = ((RemoteNeedSpecificEvent)event).getRemoteNeedURI();
if (remoteNeedURI == null){
logger.debug("remote need URI not found in event data, fetching linked data for {}", connectionURI);
remoteNeedURI = WonLinkedDataUtils.getRemoteNeedURIforConnectionURI(connectionURI,
getEventListenerContext().getLinkedDataSource());
}
if (this.coordinatorURI.equals(needURI) && this.participantURI.equals(remoteNeedURI)){
return true;
} else if (this.participantURI.equals(needURI) && this.coordinatorURI.equals(remoteNeedURI)){
return true;
} else {
return false;
}
}
private void rememberConnectionURI(final URI needURI, final URI connectionURI) {
if (this.coordinatorURI.equals(needURI)){
this.coordinatorSideConnectionURI = connectionURI;
addConnectionURIToFilter(connectionURI);
} else if (this.participantURI.equals(needURI)){
this.participantSideConnectionURI = connectionURI;
addConnectionURIToFilter(connectionURI);
} else {
throw new IllegalStateException(new MessageFormat("Listener called for need {0}, " +
"which is neither my coordinator {1} nor my " +
"participant {2}").format(new Object[]{needURI, this.coordinatorURI, this.participantURI}));
}
}
private void addConnectionURIToFilter(final URI connectionURI) {
AndFilter filter = (AndFilter)this.eventFilter;
for (EventFilter subFilter: filter.getFilters()){
if (subFilter instanceof OrFilter){
((OrFilter)subFilter).addFilter(new ConnectionUriEventFilter(connectionURI));
break;
}
}
}
public void updateFilterForBothConnectionURIs() {
OrFilter filter = new OrFilter();
filter.addFilter(new ConnectionUriEventFilter(this.coordinatorSideConnectionURI));
filter.addFilter(new ConnectionUriEventFilter(this.participantSideConnectionURI));
this.eventFilter = filter;
}
private void addIrrelevantConnectionURIToFilter(final URI connectionURI) {
AndFilter filter = (AndFilter)this.eventFilter;
filter.addFilter(new NotFilter(new ConnectionUriEventFilter(connectionURI)));
}
private boolean isConnectionURIKnown(URI connectionURI) {
assert connectionURI != null : "connectionURI must not be null";
return connectionURI.equals(this.coordinatorSideConnectionURI) || connectionURI.equals(this
.participantSideConnectionURI);
}
public URI getCoordinatorURI() {
return coordinatorURI;
}
public URI getParticipantURI() {
return participantURI;
}
public URI getCoordinatorSideConnectionURI() {
return coordinatorSideConnectionURI;
}
public URI getParticipantSideConnectionURI() {
return participantSideConnectionURI;
}
public void setCoordinatorSideConnectionURI(final URI coordinatorSideConnectionURI) {
this.coordinatorSideConnectionURI = coordinatorSideConnectionURI;
}
public void setParticipantSideConnectionURI(final URI participantSideConnectionURI) {
this.participantSideConnectionURI = participantSideConnectionURI;
}
@Override
public String toString() {
return "BATestScriptListener" + "{" +
"name=" + name +
", coordinatorURI=" + coordinatorURI +
", participantURI=" + participantURI +
", coordinatorSideConnectionURI=" + coordinatorSideConnectionURI +
", participantSideConnectionURI=" + participantSideConnectionURI +
", messagesInFlight=" + messagesInFlight +
", millisBetweenMessages=" + millisBetweenMessages +
'}';
}
}