/*
* Copyright (C) 2010 Stanford University
*
* 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 edu.stanford.junction.provider.irc;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
import java.net.URI;
import java.util.Observer;
import java.util.List;
import java.util.Observable;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;
import java.util.UUID;
import org.json.JSONException;
import org.json.JSONObject;
import edu.stanford.junction.api.activity.ActivityScript;
import edu.stanford.junction.api.activity.JunctionActor;
import edu.stanford.junction.api.messaging.MessageHeader;
import f00f.net.irc.martyr.InCommand;
import f00f.net.irc.martyr.commands.MessageCommand;
import f00f.net.irc.martyr.commands.NoticeCommand;
import f00f.net.irc.martyr.commands.JoinCommand;
import f00f.net.irc.martyr.services.AutoJoin;
import f00f.net.irc.martyr.services.AutoRegister;
import f00f.net.irc.martyr.services.AutoReconnect;
import f00f.net.irc.martyr.services.AutoResponder;
import f00f.net.irc.martyr.clientstate.ClientState;
import f00f.net.irc.martyr.IRCConnection;
import f00f.net.irc.martyr.util.FullNick;
public class Junction extends edu.stanford.junction.Junction {
private final URI mAcceptedInvitation;
private final String mSession;
private ActivityScript mActivityScript;
private IRCConnection mConnection;
private FullNick mFullNick;
private String mNickname;
private ClientState mClientState;
private MessageMultiplexer mMultiplex = new MessageMultiplexer();
private MessageDemultiplexer mDemultiplex = new MessageDemultiplexer();
public static String JX_NS = "jx";
public static String JX_SYS_MSG = "jxsysmsg";
// Stores responses to distributed activity script requests.
private LinkedBlockingQueue<JSONObject> scriptQ = new LinkedBlockingQueue<JSONObject>();
public static String makeIRCName(String name){
String s = String.valueOf(name.hashCode());
s = s.replace("-","0");
s = "x" + s.substring(0, Math.min(s.length() - 1, 7));
return s;
}
public Junction(URI uri, ActivityScript script, final JunctionActor actor) {
this.setActor(actor);
mNickname = makeIRCName(actor.getActorID());
// User & name are not important for
// our needs.
String user = "jxuser";
String name = "jxuser";
mFullNick = new FullNick(mNickname + "!" + user + "@127.0.0.1");
mAcceptedInvitation = uri;
mActivityScript = script;
mSession = "jxsession-" + makeIRCName(uri.getPath().substring(1));
String host = uri.getHost();
int port = uri.getPort();
mClientState = new ClientState();
mConnection = new IRCConnection( mClientState );
mConnection.addStateObserver(new JXStateObserver());
mConnection.addCommandObserver(new JXCommandObserver());
AutoJoin autoJoin = new AutoJoin( mConnection, "#" + mSession );
AutoRegister autoReg = new AutoRegister( mConnection, mNickname, user, name);
AutoReconnect autoRecon = new AutoReconnect( mConnection );
AutoResponder autoRes = new AutoResponder( mConnection );
autoRecon.go( host, port );
}
// This observer is updated whenever a command
// is received by the irc connection. 'update' is
// called from the irc thread. Therefore junction
// is 'driven' (by way of triggerActorJoin
// and triggerMessageReceived) from the irc thread.
//
// This is safe as long the irc thread is the ONLY
// thread sending updates into junction. Additionally,
// the client must be aware that JunctionActor's methods
// will be called on the irc thread.
//
class JXCommandObserver implements Observer{
public void update(Observable o, Object arg) {
InCommand cmd = (InCommand)arg;
if(cmd instanceof JoinCommand){
JoinCommand c = (JoinCommand)cmd;
if(c.weJoined(mClientState)){
triggerActorJoin(mActivityScript == null ||
mActivityScript.isActivityCreator());
}
}
else if(cmd instanceof MessageCommand){
MessageCommand c = (MessageCommand)cmd;
mDemultiplex.addFragment(c.getMessage(),c.getSource().getNick());
List<MessageDemultiplexer.CompleteMessage> msgs = mDemultiplex.drainCompleteMessages();
for(MessageDemultiplexer.CompleteMessage each : msgs){
String from = each.from;
String src = each.msg;
try {
JSONObject obj = new JSONObject(src);
if(obj.optBoolean("scriptRequest")){
handleScriptRequest(from, obj);
return;
}
else if(obj.optBoolean("scriptResponse")){
scriptQ.put(obj);
return;
}
else if (obj.has(NS_JX)) {
JSONObject header = obj.optJSONObject(NS_JX);
if (header.has("targetRole")) {
String target = header.optString("targetRole");
if(!inLocalRoles(target)) return;
}
}
MessageHeader header = new MessageHeader(Junction.this, obj, from);
triggerMessageReceived(header, obj);
} catch (Exception e) {
System.err.println("Could not handle incoming message: " + src);
}
}
}
}
}
class JXStateObserver implements Observer{
public void update(Observable o, Object arg) {
System.out.println("State update: " + arg.toString());
}
}
protected void handleScriptRequest(String from, JSONObject req){
if(mActivityScript == null) return;
try{
JSONObject response = new JSONObject();
response.put("scriptResponse", true);
response.put("requestId", req.optString("requestId"));
response.put("script", mActivityScript.getJSON());
sendMessageToActor(from, response);
}
catch(JSONException e){
e.printStackTrace(System.err);
}
}
private Boolean inLocalRoles(String target){
String[] roles = getActor().getRoles();
for (int i = 0; i < roles.length; i++) {
if (roles[i].equals(target)) {
return true;
}
}
return false;
}
@Override
public void disconnect() {
mConnection.disconnect();
}
@Override
public URI getAcceptedInvitation() {
return mAcceptedInvitation;
}
@Override
public ActivityScript getActivityScript() {
scriptQ.clear();
JSONObject req = new JSONObject();
String requestId = UUID.randomUUID().toString();
try{
req.put("scriptRequest", "true");
req.put("requestId", requestId);
sendMessageToSession(req);
}
catch(JSONException e){
e.printStackTrace(System.err);
return null;
}
try{
int maxTries = 5;
long maxWaitPerTry = 2000L;
for(int i = 0; i < maxTries; i++){
JSONObject response = scriptQ.poll(
maxWaitPerTry, TimeUnit.MILLISECONDS);
if(response == null) {
return null;
}
else if(response.optString("requestId").equals(requestId)){
JSONObject script = response.optJSONObject("script");
return new ActivityScript(script);
}
}
}
catch(InterruptedException e){
return null;
}
return null;
}
@Override
public URI getBaseInvitationURI() {
try {
return new URI("junction://localhost#irc");
} catch (Exception e) {
return null;
}
}
@Override
public String getSessionID() {
return mSession;
}
@Override
public String getSwitchboard() {
return mAcceptedInvitation.getHost();
}
private void sendMsgTo(String msg, String to){
List<String> fragments = mMultiplex.divide(msg);
for(String frag : fragments){
MessageCommand cmd = new MessageCommand(mFullNick, to, frag);
mConnection.sendCommand(cmd);
}
}
@Override
public void doSendMessageToActor(String actorID, JSONObject message) {
sendMsgTo(message.toString(), actorID);
}
@Override
public void doSendMessageToRole(String role, JSONObject message) {
JSONObject jx;
if (message.has(NS_JX)) {
jx = message.optJSONObject(NS_JX);
} else {
jx = new JSONObject();
try {
message.put(NS_JX, jx);
} catch (JSONException j) {}
}
try {
jx.put("targetRole", role);
} catch (Exception e) {}
String msg = message.toString();
sendMsgTo(msg, "#" + mSession + "," + mNickname);
}
@Override
public void doSendMessageToSession(JSONObject message) {
String msg = message.toString();
sendMsgTo(msg, "#" + mSession + "," + mNickname);
}
}