/**
* OLAT - Online Learning and Training<br />
* http://www.olat.org
* <p>
* Licensed under the Apache License, Version 2.0 (the "License"); <br />
* you may not use this file except in compliance with the License.<br />
* You may obtain a copy of the License at
* <p>
* http://www.apache.org/licenses/LICENSE-2.0
* <p>
* Unless required by applicable law or agreed to in writing,<br />
* software distributed under the License is distributed on an "AS IS" BASIS,
* <br />
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. <br />
* See the License for the specific language governing permissions and <br />
* limitations under the License.
* <p>
* Copyright (c) since 2004 at Multimedia- & E-Learning Services (MELS),<br />
* University of Zurich, Switzerland.
* <p>
*/
package org.olat.instantMessaging.groupchat;
import java.util.ArrayList;
import java.util.Date;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import org.jivesoftware.smack.PacketListener;
import org.jivesoftware.smack.XMPPConnection;
import org.jivesoftware.smack.XMPPException;
import org.jivesoftware.smack.packet.Message;
import org.jivesoftware.smack.packet.Presence;
import org.jivesoftware.smackx.muc.MultiUserChat;
import org.olat.basesecurity.ManagerFactory;
import org.olat.core.commons.persistence.DBFactory;
import org.olat.core.commons.taskExecutor.TaskExecutorManager;
import org.olat.core.gui.UserRequest;
import org.olat.core.gui.components.Component;
import org.olat.core.gui.components.htmlheader.jscss.JSAndCSSComponent;
import org.olat.core.gui.components.link.Link;
import org.olat.core.gui.components.link.LinkFactory;
import org.olat.core.gui.components.panel.Panel;
import org.olat.core.gui.components.velocity.VelocityContainer;
import org.olat.core.gui.control.Controller;
import org.olat.core.gui.control.Event;
import org.olat.core.gui.control.WindowControl;
import org.olat.core.gui.control.controller.BasicController;
import org.olat.core.gui.control.floatingresizabledialog.FloatingResizableDialogController;
import org.olat.core.id.Identity;
import org.olat.core.id.OLATResourceable;
import org.olat.core.id.UserConstants;
import org.olat.core.logging.AssertException;
import org.olat.core.util.event.GenericEventListener;
import org.olat.instantMessaging.ClientHelper;
import org.olat.instantMessaging.InstantMessagingEvent;
import org.olat.instantMessaging.InstantMessagingModule;
/**
* Description:<br />
* Handles an group chat in an floating window with all events like receiving messages, sending messages and updating an roster with all joined users
*
* There are several options how to display or start the chat as it gets used in different places
* Initial Date: 13.03.2007 <br />
*
* @author guido
*/
public class InstantMessagingGroupChatController extends BasicController implements GenericEventListener {
private static final String NICKNAME_PREFIX = "anonym_";
private String NICKNAME_ANONYMOUS;
private XMPPConnection connection;
private MultiUserChat muc;
private VelocityContainer errorCompact = createVelocityContainer("errorCompact");
private VelocityContainer error = createVelocityContainer("error");
private VelocityContainer groupchatVC = createVelocityContainer("groupchat");
private VelocityContainer groupChatMsgFieldVC = createVelocityContainer("groupChatMsgField");
private VelocityContainer summaryCompactVC = createVelocityContainer("summaryCompact");
private VelocityContainer rosterVC = createVelocityContainer("roster");
private VelocityContainer summaryVC = createVelocityContainer("summary");
private Panel groupChatMsgPanel;
private Integer occupantsCount = 1;
private ToggleAnonymousForm toggleAnonymousForm;
private SendMessageForm sendMessageForm;
private String roomJID;
private StringBuilder messageHistory;
private Link openGroupChatPanel;
private Link indicateNewMessage;
private Link openGroupChatPanelButton;
private Link refresh;
private Panel main, roster;
private GroupChatJoinTask roomJoinTask;
private boolean chatWindowOpen = false;
private Locale locale;
private Controller floatingResizablePanelCtr;
private OLATResourceable ores;
private final Panel chatWindowHolder;
private final boolean compact;
private String roomName;
private long latestRosterUpdate = 0;
private JSAndCSSComponent jsc;
private List<String> rosterList = new ArrayList<String>();
private boolean anonymousInChatroom;
private boolean lazyCreation;
private boolean initDone;
/**
*
* @param ureq
* @param wControl
* @param ores
* @param roomName
* @param fixcsspanel if you want the panel rendered somewhere else to solve css issues add it here otherwise null
* @param lazyCreation if true the user does not get joined automatically to the chatRoom
*/
public InstantMessagingGroupChatController(UserRequest ureq, WindowControl wControl, OLATResourceable ores, String roomName, Panel chatWindowHolder,
boolean compact, boolean anonymousInChatroom, boolean lazyCreation) {
super(ureq, wControl);
this.chatWindowHolder = chatWindowHolder;
this.compact = compact;
if (roomName == null) throw new AssertException("roomName can not be null");
this.roomName = roomName;
if (ores == null) throw new AssertException("olat resourcable can not be null");
this.ores = ores;
this.locale = ureq.getLocale();
this.anonymousInChatroom = anonymousInChatroom;
this.lazyCreation = lazyCreation;
main = new Panel("main");
openGroupChatPanel = LinkFactory.createCustomLink("participantsCount", "cmd.open.client", "", Link.NONTRANSLATED, summaryCompactVC, this);
if (lazyCreation) { //show only link to join the groupChat
openGroupChatPanel.setCustomDisplayText(getTranslator().translate("click.to.join"));
openGroupChatPanel.setCustomEnabledLinkCSS("b_toolbox_link");
openGroupChatPanel.setTooltip(getTranslator().translate("course.chat.click.to.join"), false);
openGroupChatPanel.registerForMousePositionEvent(true);
main.setContent(summaryCompactVC);
putInitialPanel(main);
} else {
//create controller stuff and join chatRoom immediately
if ( init(ureq) ) {
putInitialPanel(main);
} else {
//error case
putInitialPanel(errorCompact);
}
}
}
/**
*
* @param ureq
*/
private boolean init(UserRequest ureq) {
NICKNAME_ANONYMOUS = NICKNAME_PREFIX+(int)Math.rint(Math.random()*getIdentity().getKey());
connection = InstantMessagingModule.getAdapter().getClientManager().getInstantMessagingClient(getIdentity().getName()).getConnection();
roomJID = InstantMessagingModule.getAdapter().createChatRoomJID(ores);
groupChatMsgPanel = new Panel("groupchat");
roster = new Panel("roster");
roster.setContent(rosterVC);
messageHistory = new StringBuilder();
groupchatVC.put("groupChatMessages", groupChatMsgPanel);
if (compact) {
openGroupChatPanel.setCustomDisplayText(1+" "+getTranslator().translate("participants.in.chat"));
openGroupChatPanel.setCustomEnabledLinkCSS("b_toolbox_link");
openGroupChatPanel.setTooltip(getTranslator().translate("course.chat.intro"), false);
openGroupChatPanel.registerForMousePositionEvent(true);
main.setContent(summaryCompactVC);
} else {
openGroupChatPanelButton = LinkFactory.createButton("openChat", summaryVC, this);
openGroupChatPanelButton.registerForMousePositionEvent(true);
summaryVC.put("roster", roster);
main.setContent(summaryVC);
}
groupChatMsgPanel.setContent(groupChatMsgFieldVC);
groupChatMsgFieldVC.contextPut("id", this.hashCode());
//create form for username toogle
toggleAnonymousForm = new ToggleAnonymousForm(ureq, getWindowControl());
toggleAnonymousForm.addControllerListener(this);
//toggle form only if logged in anonymous
if (anonymousInChatroom) groupchatVC.put("toggleSwitch", toggleAnonymousForm.getInitialComponent());
//create form for msg sending
sendMessageForm = new SendMessageForm(ureq, getWindowControl());
sendMessageForm.addControllerListener(this);
groupchatVC.put("sendMessageForm", sendMessageForm.getInitialComponent());
refresh = LinkFactory.createCustomLink("refresh", "cmd.refresh", "", Link.NONTRANSLATED, groupchatVC, this);
refresh.setCustomEnabledLinkCSS("b_small_icon o_instantmessaging_refresh_icon");
refresh.setTitle(getTranslator().translate("im.refresh"));
Link refreshList = LinkFactory.createButtonXSmall("im.refresh", summaryVC, this);
//set polling time of peridocal updater
jsc = new JSAndCSSComponent("intervall2", this.getClass(), null, null, false, null, InstantMessagingModule.getIDLE_POLLTIME());
groupchatVC.put("checkfordirtycomponents", jsc);
//set defaults
groupChatMsgFieldVC.contextPut("groupChatMessages", "");
boolean ajaxOn = getWindowControl().getWindowBackOffice().getWindowManager().isAjaxEnabled();
groupchatVC.contextPut("isAjaxMode", Boolean.valueOf(ajaxOn));
summaryVC.contextPut("isAjaxMode", Boolean.valueOf(ajaxOn));
if (connection != null && connection.isConnected()) {
try {
muc = new MultiUserChat(connection, roomJID);
if (anonymousInChatroom) {
roomJoinTask = new GroupChatJoinTask(ores, muc, connection, roomJID, NICKNAME_ANONYMOUS, sanitizeRoomName(roomName), this);
} else {
roomJoinTask = new GroupChatJoinTask(ores, muc, connection, roomJID, getIdentity().getName(), sanitizeRoomName(roomName), this);
rosterVC.setDirty(true);
}
TaskExecutorManager.getInstance().runTask(roomJoinTask);
} catch (IllegalStateException e) {
logWarn("Error while trying to create group chat room for user"+getIdentity().getName()+" and course resource: +ores", e);
}
} else {
//error case
return false;
}
initDone = true;
return true;
}
/**
* clean room name from ampersands
* @param room
* @return
*/
private String sanitizeRoomName(String room) {
if (room.contains("&")) {
return room.replaceAll("&", "&");
}
return room;
}
/**
* @see org.olat.core.gui.control.DefaultController#doDispose(boolean)
*/
@Override
protected void doDispose() {
//MUST be called by super controller!
if (muc != null && muc.isJoined() && connection.isConnected()) {
try {
muc.leave();
PacketListener msgListener = roomJoinTask.getMessageListener();
if (msgListener != null) muc.removeMessageListener(msgListener);
PacketListener pListener = roomJoinTask.getParticipationsListener();
if (pListener != null) muc.removeParticipantListener(pListener);
muc = null;
} catch (Exception e) {
logWarn("Error while leaving multiuserchat:", e);
}
}
if (toggleAnonymousForm != null) toggleAnonymousForm.dispose();
if (sendMessageForm != null) sendMessageForm.dispose();
if (floatingResizablePanelCtr != null ) floatingResizablePanelCtr.dispose();
}
/**
* @see org.olat.core.gui.control.DefaultController#event(org.olat.core.gui.UserRequest,
* org.olat.core.gui.components.Component,
* org.olat.core.gui.control.Event)
*/
@Override
public void event(UserRequest ureq, Component source, Event event) {
if (lazyCreation && !initDone) {
openGroupChatPanel.setCustomDisplayText(1+" "+getTranslator().translate("participants.in.chat"));
openGroupChatPanel.setCustomEnabledLinkCSS("b_toolbox_link");
openGroupChatPanel.setTooltip(getTranslator().translate("course.chat.intro"), false);
openGroupChatPanel.registerForMousePositionEvent(true);
init(ureq);
}
//offer refresh button for non ajax mode
boolean ajaxOn = getWindowControl().getWindowBackOffice().getWindowManager().isAjaxEnabled();
groupchatVC.contextPut("isAjaxMode", Boolean.valueOf(ajaxOn));
summaryVC.contextPut("isAjaxMode", Boolean.valueOf(ajaxOn));
if ( (muc != null && muc.isJoined()) || lazyCreation) {
if (source == openGroupChatPanel || source == openGroupChatPanelButton || source == indicateNewMessage) {
int x=0; int y=0;
if (source == openGroupChatPanel) {
x = openGroupChatPanel.getOffsetX()-450;
y = openGroupChatPanel.getOffsetY()+30;
if (x == -450 && y == 30) {x=300;y=300;} //selenium does not send xy coordinates -> set panel somewhere visible
} else if (source == openGroupChatPanelButton) {
x = openGroupChatPanelButton.getOffsetX();
y = openGroupChatPanelButton.getOffsetY();
} else if (source == indicateNewMessage) {
x = indicateNewMessage.getOffsetX()-550;
y = indicateNewMessage.getOffsetY();
}
//only open floating window if not yet open
if (!chatWindowOpen) {
if (floatingResizablePanelCtr != null ) floatingResizablePanelCtr.dispose();
floatingResizablePanelCtr = new FloatingResizableDialogController(ureq, getWindowControl(), groupchatVC,
getTranslator().translate("course.groupchat")+" "+roomNameShort(roomName), 550, 300, x, y, roster, getTranslator().translate("groupchat.roster"), true, false, true, "chat_window");
floatingResizablePanelCtr.addControllerListener(this);
jsc.setRefreshIntervall(InstantMessagingModule.getCHAT_POLLTIME());
if (chatWindowHolder != null) {
chatWindowHolder.setContent(floatingResizablePanelCtr.getInitialComponent());
} else {
main.setContent(floatingResizablePanelCtr.getInitialComponent());
}
}
//reset new msg icon
//indicateNewMessage.setCustomEnabledLinkCSS("");
chatWindowOpen = true;
prepareRosterList();
groupchatVC.contextPut("title", roomNameShort(roomName));
if (indicateNewMessage != null) indicateNewMessage.setCustomEnabledLinkCSS("");
}
} else {
if (compact) {
main.setContent(errorCompact);
} else {
main.setContent(error);
}
}
}
private String roomNameShort(String roomNameLong) {
if (roomNameLong.length() > 30) return roomNameLong.substring(0, 29)+"...";
return roomNameLong;
}
/**
*
* @see org.olat.core.gui.control.DefaultController#event(org.olat.core.gui.UserRequest, org.olat.core.gui.control.Controller, org.olat.core.gui.control.Event)
*/
@Override
protected void event(UserRequest ureq, Controller source, Event event) {
//TODO:gs:a wrap also muc, to catch all exceptions which can occur
if (muc != null && muc.isJoined()) {
try {
if (source == toggleAnonymousForm) {
if (muc.getNickname().startsWith(NICKNAME_PREFIX)) {
try {
muc.changeNickname(getIdentity().getName());
} catch (XMPPException e) {
logWarn("Could not change nickname for user: "+getIdentity().getName() + "in course chat: "+ores, e);
appendToMsgHistory(createMessage("chatroom", getTranslator().translate("msg.send.error")));
} catch (Exception e) {
logWarn("Could not change nickname for user: "+getIdentity().getName() + "in course chat: "+ores, e);
appendToMsgHistory(createMessage("chatroom", getTranslator().translate("msg.send.error")));
}
}
else {
muc.changeNickname(NICKNAME_ANONYMOUS);
}
} else if (source == sendMessageForm) {
if (isLogDebugEnabled()) logDebug("sending msg: +"+sendMessageForm.getMessage()+ "+ to chatroom: "+roomJID, null);
try {
muc.sendMessage(sendMessageForm.getMessage());
sendMessageForm.resetTextField();
} catch (XMPPException e) {
logWarn("Could not send IM message for user: "+getIdentity().getName() + "in course chat: "+ores, e);
appendToMsgHistory(createMessage("chatroom", getTranslator().translate("msg.send.error")));
}
}
//hack, IM server need some time to send msg to client. Only an issue in non ajax mode
//otherwise the user can't see the message he has just send
boolean ajaxOn = getWindowControl().getWindowBackOffice().getWindowManager().isAjaxEnabled();
if (!ajaxOn) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
//nothing to do
}
}
} catch (XMPPException e) {
logWarn("Could not send IM message for user: "+getIdentity().getName() + "in course chat: "+ores, e);
appendToMsgHistory(createMessage("chatroom", getTranslator().translate("msg.send.error")));
} catch (IllegalStateException e) {
//this happens when the server is going down while the user had already joind a room and tries to send a msg
logWarn("Could not send IM message for user: "+getIdentity().getName() + "in course chat: "+ores, e);
appendToMsgHistory(createMessage("chatroom", getTranslator().translate("msg.send.error")));
}
groupChatMsgFieldVC.setDirty(true);//set dirty for non ajax mode
rosterVC.setDirty(true);//set dirty for non ajax mode
occupantsCount = Integer.valueOf(muc.getOccupantsCount());
if (occupantsCount.equals(Integer.valueOf(0))) occupantsCount = Integer.valueOf(1);
openGroupChatPanel.setCustomDisplayText(occupantsCount+" "+getTranslator().translate("participants.in.chat"));
} else {
Message msg = createMessage("chatroom", getTranslator().translate("coursechat.not.available"));
appendToMsgHistory(msg);
main.setContent(errorCompact);
}
if (source == floatingResizablePanelCtr) {
if (chatWindowHolder != null) {
chatWindowHolder.setContent(null);
} else {
if (compact) main.setContent(summaryCompactVC);
else main.setContent(summaryVC);
}
if (indicateNewMessage != null) {
summaryCompactVC.remove(indicateNewMessage);
indicateNewMessage = null;
}
chatWindowOpen = false;
//decrease polling time of periodical updater
jsc.setRefreshIntervall(InstantMessagingModule.getIDLE_POLLTIME());
}
}
/**
* @return true if the chat window is openend by the user
* used by velocity
*/
public boolean isChatWindowOpen() {
return chatWindowOpen;
}
private Message createMessage(String from, String msgBody) {
Message msg = new Message();
msg.setBody(msgBody);
msg.setFrom(from);
msg.setProperty("receiveTime", new Long(new Date().getTime()));
return msg;
}
public void event(Event event) {
InstantMessagingEvent imEvent = (InstantMessagingEvent)event;
if (imEvent.getCommand().equals("groupchat")) {
Message msg = (Message) imEvent.getPacket();
if (isLogDebugEnabled()) logDebug("incoming msg for groupchat: "+msg.getType(), null);
msg.setProperty("receiveTime", new Long(new Date().getTime()));
if ((msg.getType() == Message.Type.groupchat) && msg.getBody() != null) {
if (!chatWindowOpen && !msg.getBody().startsWith("This room is")) { //get rid of room status msg
indicateNewMessage = LinkFactory.createCustomLink("indicateNewMsg", "cmd.open.client", " ", Link.NONTRANSLATED, summaryCompactVC, this);
indicateNewMessage.registerForMousePositionEvent(true);
indicateNewMessage.setCustomEnabledLinkCSS("b_small_icon o_instantmessaging_new_msg_icon");
indicateNewMessage.setTooltip(getTranslator().translate("groupchat.new.msg"), true);
}
appendToMsgHistory(msg);
}
} else if (imEvent.getCommand().equals("participant")) {
Presence presence = (Presence) imEvent.getPacket();
if (isLogDebugEnabled()) logDebug("incoming presence change for groupchat: "+presence.getFrom() +" : "+ presence.getType(), null);
if (presence.getFrom() != null) {
if (presence.getFrom().startsWith("coursemodule-") && presence.getType() == Presence.Type.unavailable && !presence.getFrom().endsWith(NICKNAME_ANONYMOUS)) {
//workaround for smack bug, that does not instantly update the occupants count when someone leaves the room
if (occupantsCount != 1) occupantsCount--;
} else {
occupantsCount = Integer.valueOf(muc.getOccupantsCount());
if (occupantsCount.equals(Integer.valueOf(0))) occupantsCount = Integer.valueOf(1);
}
if (isLogDebugEnabled()) logDebug("incoming presence change for groupchat. No. of participants: "+occupantsCount, null);
openGroupChatPanel.setCustomDisplayText(occupantsCount+" "+getTranslator().translate("participants.in.chat"));
try {
if (chatWindowOpen) {
//FIXME:gs:a temporary fox for connection increasing problem due to asynchronous nature of IM events
// together with <hibernate.c3p0.autoCommitOnClose>false</hibernate.c3p0.autoCommitOnClose> switch it showed up
prepareRosterList(presence);
DBFactory.getInstance(false).commitAndCloseSession();
}
} catch (Exception e) {
DBFactory.getInstance(false).rollbackAndCloseSession();
}
}
}
}
private void appendToMsgHistory(Message msg) {
if(!msg.getBody().startsWith("This room is")) {
synchronized (messageHistory) { // //o_clusterOK by:fj: message history is per user
messageHistory.append("<div><span style=\"color:"+colorizeUserName(extractUsername(msg.getFrom()))+"\">[");
messageHistory.append(ClientHelper.getSendDate(msg, locale));
messageHistory.append("] ");
messageHistory.append(extractUsername(msg.getFrom()));
messageHistory.append(": </span>");
messageHistory.append(msg.getBody());
messageHistory.append("</div>");
groupChatMsgFieldVC.contextPut("groupChatMessages", messageHistory.toString());
}
}
}
/**
*
* @param presence
*/
private void prepareRosterList(Presence presence) {
synchronized (rosterList) { //o_clusterOK by:fj
if (presence.getType() == Presence.Type.available) {
if (!rosterList.contains(extractUsername(presence.getFrom()))) {
rosterList.add(getFullUserName(extractUsername(presence.getFrom())));
}
} else if (presence.getType() == Presence.Type.unavailable) {
rosterList.remove(getFullUserName(extractUsername(presence.getFrom())));
}
}
rosterVC.contextPut("rosterList", rosterList);
}
/**
*
* @param username
* @return full user name and username of user like Guido Schnider (guidosch)
* or the username if not found
*
*/
private String getFullUserName(String username) {
Identity ident = ManagerFactory.getManager().findIdentityByName(username);
if (ident != null) {
StringBuilder sb = new StringBuilder();
sb.append(ident.getUser().getProperty(UserConstants.FIRSTNAME, locale)).append(" ");
sb.append(ident.getUser().getProperty(UserConstants.LASTNAME, locale)).append(" ");
sb.append("(").append(ident.getName()).append(")");
return sb.toString();
}
return username;
}
private void prepareRosterList() {
//there are many incoming events for a groupchat, only update the roster with a break of five seconds
if (System.currentTimeMillis()-latestRosterUpdate > 2000) {
rosterList = new ArrayList<String>();
latestRosterUpdate = System.currentTimeMillis();
try {
for (Iterator i = muc.getOccupants(); i.hasNext();) {
String occupant = (String) i.next();
rosterList.add(getFullUserName(extractUsername(occupant)));
}
if ( rosterList.size() == 0 ) {
rosterList.add(NICKNAME_ANONYMOUS);
}
rosterVC.contextPut("rosterList", rosterList);
} catch (Exception e) {
logWarn("Error while trying to get participants for chat room for user"+getIdentity().getName()+" and course resource: +ores", e);
}
}
}
private String colorizeUserName(String from) {
//append name to lengt 6
if (from.startsWith(NICKNAME_PREFIX)) {
from = new StringBuilder(from).reverse().toString();
}
if (from.length() < 6){
while (from.length() < 6) {
from = from+"9";
}
}
//get hex form the first 6 chars (only numbers)
StringBuilder sb = new StringBuilder();
sb.append("#");
for (int j = 0; j < 6; j++) {
int z = from.charAt(j)%9;
if(z == 8) sb.append("A");//make more darker colors
else if (z == 9) sb.append("B");
else sb.append(z);
}
return sb.toString();
}
private String extractUsername(String from) {
if(from != null) {
if(from.contains("/"))
return from.substring(from.lastIndexOf("/")+1, from.length());
}
return "chatroom";
}
}