diff --git a/chat/chat-api/api/src/java/org/sakaiproject/chat2/model/ChatManager.java b/chat/chat-api/api/src/java/org/sakaiproject/chat2/model/ChatManager.java index d8cb9453d2c5..8a930f7d25b6 100644 --- a/chat/chat-api/api/src/java/org/sakaiproject/chat2/model/ChatManager.java +++ b/chat/chat-api/api/src/java/org/sakaiproject/chat2/model/ChatManager.java @@ -23,6 +23,7 @@ import java.util.Date; import java.util.List; +import java.util.Map; import org.sakaiproject.entity.api.EntitySummary; import org.sakaiproject.exception.PermissionException; @@ -145,20 +146,6 @@ public interface ChatManager extends EntitySummary { * @return ChatMessage */ public ChatMessage getMessage(String chatMessageId); - - /** - * Adds a room listener on the room - * @param observer RoomObserver the class to observe the room - * @param roomId the room being observed - */ - public void addRoomListener(RoomObserver observer, String roomId); - - /** - * Removes a room listener on the room - * @param observer RoomObserver the class to stop observing the room - * @param roomId the room being observed - */ - public void removeRoomListener(RoomObserver observer, String roomId); /** * sends the message out to the other clients @@ -264,4 +251,54 @@ public interface ChatManager extends EntitySummary { */ public int getMessagesMax(); + /** + * Get all online users in given siteId and chat channel id + * + * @param siteId + * @param channelId + * @return + */ + public List getPresentUsers(String siteId, String channelId); + + /** + * - Update heartbeat for current user + * - Get undelivered (latest) messages + * - Get online users + * - Get removed messages + * + * @param siteId + * @param channelId + * @param sessionKey + * @return + */ + public Map handleChatData(String siteId, String channelId, String sessionKey); + + /** + * Get pollInterval (in milliseconds) from properties + * @return + */ + public int getPollInterval(); + + /** + * Get session key (ussage_session_id:session_user_id) from current session + * @return + */ + public String getSessionKey(); + + /** + * Get different date strings based on given ChatMessage::messageDate + * @param msg + * @return + */ + public MessageDateString getMessageDateString(ChatMessage msg); + + /** + * Get user timezone from preferencesService. + * This method is almost a duplicate from BasicTimeService.getUserTimezoneLocale. + * Would be great if the preferencesService returns it directly + * @return + */ + public String getUserTimeZone(); + + } diff --git a/chat/chat-api/api/src/java/org/sakaiproject/chat2/model/ChatMessage.java b/chat/chat-api/api/src/java/org/sakaiproject/chat2/model/ChatMessage.java index be3b08e7a70a..d3660548a233 100644 --- a/chat/chat-api/api/src/java/org/sakaiproject/chat2/model/ChatMessage.java +++ b/chat/chat-api/api/src/java/org/sakaiproject/chat2/model/ChatMessage.java @@ -144,6 +144,10 @@ public void setBody(String body) { } this.body = formattedBody; } + + public void setRawBody(String body){ + this.body = body; + } /** * Serialize the resource into XML, adding an element to the doc under the top of the stack element. diff --git a/chat/chat-api/api/src/java/org/sakaiproject/chat2/model/DeleteMessage.java b/chat/chat-api/api/src/java/org/sakaiproject/chat2/model/DeleteMessage.java new file mode 100644 index 000000000000..12a6c3e234e5 --- /dev/null +++ b/chat/chat-api/api/src/java/org/sakaiproject/chat2/model/DeleteMessage.java @@ -0,0 +1,11 @@ +package org.sakaiproject.chat2.model; + +import lombok.AllArgsConstructor; +import lombok.Data; + +@Data +@AllArgsConstructor +public class DeleteMessage{ + private String id; + private String channelId; +} diff --git a/chat/chat-api/api/src/java/org/sakaiproject/chat2/model/MessageDateString.java b/chat/chat-api/api/src/java/org/sakaiproject/chat2/model/MessageDateString.java new file mode 100644 index 000000000000..18db3d8308c7 --- /dev/null +++ b/chat/chat-api/api/src/java/org/sakaiproject/chat2/model/MessageDateString.java @@ -0,0 +1,12 @@ +package org.sakaiproject.chat2.model; + +import lombok.AllArgsConstructor; +import lombok.Data; + +@Data +@AllArgsConstructor +public class MessageDateString{ + private String localizedDate; + private String localizedTime; + private String dateID; +} diff --git a/chat/chat-api/api/src/java/org/sakaiproject/chat2/model/PresenceObserver.java b/chat/chat-api/api/src/java/org/sakaiproject/chat2/model/PresenceObserver.java deleted file mode 100644 index 024486086a9f..000000000000 --- a/chat/chat-api/api/src/java/org/sakaiproject/chat2/model/PresenceObserver.java +++ /dev/null @@ -1,47 +0,0 @@ -/********************************************************************************** - * $URL$ - * $Id$ - *********************************************************************************** - * - * Copyright (c) 2007, 2008 The Sakai Foundation - * - * Licensed under the Educational Community 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.opensource.org/licenses/ECL-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 org.sakaiproject.chat2.model; - -/** - * any class that wants to observer users joining and leaving a location should - * implement this class and then open a new PresenceObserverHelper(this, "location") - * @author andersjb - * - */ -public interface PresenceObserver { - - /** - * This is called by the PresenceObserverHelper when a user joins a location - * @param location the user is joining this location - * @param user the user joining - */ - public void userJoined(String location, String user); - - - /** - * This is called by the PresenceObserverHelper when a user leaves a location - * @param location the user is leaving this location - * @param user the user leaving - */ - public void userLeft(String location, String user); - -} diff --git a/chat/chat-api/api/src/java/org/sakaiproject/chat2/model/RoomObserver.java b/chat/chat-api/api/src/java/org/sakaiproject/chat2/model/RoomObserver.java deleted file mode 100644 index f40f6285efe9..000000000000 --- a/chat/chat-api/api/src/java/org/sakaiproject/chat2/model/RoomObserver.java +++ /dev/null @@ -1,48 +0,0 @@ -/********************************************************************************** - * $URL$ - * $Id$ - *********************************************************************************** - * - * Copyright (c) 2007, 2008 The Sakai Foundation - * - * Licensed under the Educational Community 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.opensource.org/licenses/ECL-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 org.sakaiproject.chat2.model; - -/** - * Any classes that want to observer the messages in a room needs to implement this - * and then add itself to the ChatManager - * @author andersjb - * - */ -public interface RoomObserver { - - /** - * - * @param toolId String of the tool receiving the message - * @param roomId String of the room receiving the masseg - * @param message ChatMessage the message being received - */ - public void receivedMessage(String roomId, Object message); - - - /** - * - * @param toolId - * @param roomId - */ - public void roomDeleted(String roomId); - -} diff --git a/chat/chat-api/api/src/java/org/sakaiproject/chat2/model/SimpleUser.java b/chat/chat-api/api/src/java/org/sakaiproject/chat2/model/SimpleUser.java new file mode 100644 index 000000000000..e3c66d180821 --- /dev/null +++ b/chat/chat-api/api/src/java/org/sakaiproject/chat2/model/SimpleUser.java @@ -0,0 +1,13 @@ +package org.sakaiproject.chat2.model; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.EqualsAndHashCode; + +@Data +@AllArgsConstructor +@EqualsAndHashCode(of={"id"}) +public class SimpleUser{ + private String id; + private String name; +} diff --git a/chat/chat-api/api/src/java/org/sakaiproject/chat2/model/TransferableChatMessage.java b/chat/chat-api/api/src/java/org/sakaiproject/chat2/model/TransferableChatMessage.java new file mode 100644 index 000000000000..4c60947c8c7f --- /dev/null +++ b/chat/chat-api/api/src/java/org/sakaiproject/chat2/model/TransferableChatMessage.java @@ -0,0 +1,94 @@ +package org.sakaiproject.chat2.model; + +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.io.Serializable; +import java.util.Date; + +import lombok.Data; + +/** + * + * Class used to transfer data through jGroups + * + */ +@Data +public class TransferableChatMessage implements Serializable { + + public enum MessageType { + CHAT, + HEARTBEAT, + CLEAR, + REMOVE; + } + + public MessageType type; + public String id; + public String owner; + public String siteId; + public String channelId; + public String content; + public long timestamp; + + public TransferableChatMessage(ChatMessage msg) { + this(MessageType.CHAT, msg.getId(), msg.getOwner(), msg.getChatChannel().getContext(), msg.getChatChannel().getId(), msg.getBody()); + } + + public TransferableChatMessage(MessageType type, String id) { + this(type, id, null, null, null, null); + } + + public TransferableChatMessage(MessageType type, String id, String channelId) { + this(type, id, null, null, channelId, null); + } + + public TransferableChatMessage(MessageType type, String id, String owner, String siteId, String channelId, String content) { + this.type = type; + this.id = id; + this.owner = owner; + this.siteId = siteId; + this.channelId = channelId; + this.content = content; + this.timestamp = (new Date()).getTime(); + } + + public static TransferableChatMessage HeartBeat(String channelId, String sessionKey){ + return new TransferableChatMessage(MessageType.HEARTBEAT, sessionKey, channelId); + } + + public ChatMessage toChatMessage() { + return toChatMessage(null); + } + public ChatMessage toChatMessage(ChatChannel channel) { + ChatMessage message = new ChatMessage(); + + message.setId(id); + message.setChatChannel(channel); + message.setOwner(owner); + message.setRawBody(content); + message.setMessageDate(new Date(timestamp)); + + return message; + } + + public void writeObject(ObjectOutputStream out) throws IOException { + out.writeObject(type.toString()); + out.writeObject(id); + out.writeObject(owner); + out.writeObject(siteId); + out.writeObject(channelId); + out.writeObject(content); + out.writeObject(timestamp); + } + + public void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException { + this.type = MessageType.valueOf((String) in.readObject()); + this.id = (String) in.readObject(); + this.owner = (String) in.readObject(); + this.siteId = (String) in.readObject(); + this.channelId = (String) in.readObject(); + this.content = (String) in.readObject(); + this.timestamp = (Long) in.readObject(); + } +} diff --git a/chat/chat-impl/impl/conf/jgroups-config.xml b/chat/chat-impl/impl/conf/jgroups-config.xml new file mode 100644 index 000000000000..3da426251d59 --- /dev/null +++ b/chat/chat-impl/impl/conf/jgroups-config.xml @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/chat/chat-impl/impl/pom.xml b/chat/chat-impl/impl/pom.xml index 3f23863d0d22..84549c720953 100644 --- a/chat/chat-impl/impl/pom.xml +++ b/chat/chat-impl/impl/pom.xml @@ -71,12 +71,16 @@ entitybroker-api - org.sakaiproject.courier - sakai-courier-api + org.sakaiproject.presence + sakai-presence-api + + + org.jgroups + jgroups - - org.sakaiproject.courier - sakai-courier-util + + com.google.guava + guava commons-lang @@ -97,6 +101,12 @@ **/*.properties + + ${basedir}/conf + + **/*.xml + + diff --git a/chat/chat-impl/impl/src/java/org/sakaiproject/chat2/model/impl/ChatChannelEntityProvider.java b/chat/chat-impl/impl/src/java/org/sakaiproject/chat2/model/impl/ChatChannelEntityProvider.java index 558ecd714c3d..e723c18c709b 100644 --- a/chat/chat-impl/impl/src/java/org/sakaiproject/chat2/model/impl/ChatChannelEntityProvider.java +++ b/chat/chat-impl/impl/src/java/org/sakaiproject/chat2/model/impl/ChatChannelEntityProvider.java @@ -30,7 +30,6 @@ import org.slf4j.LoggerFactory; import org.sakaiproject.chat2.model.ChatChannel; import org.sakaiproject.chat2.model.ChatManager; -import org.sakaiproject.courier.api.CourierService; import org.sakaiproject.entitybroker.EntityReference; import org.sakaiproject.entitybroker.EntityView; import org.sakaiproject.entitybroker.entityprovider.annotations.EntityCustomAction; @@ -214,41 +213,6 @@ public List getEntities(EntityReference ref, Search search) { return channels; } - /** - * Custom action to start listening to a channel - * @return true if a listener is started. - */ - @EntityCustomAction(action="listen",viewKey=EntityView.VIEW_EDIT) - public boolean listen(EntityReference ref, Map params) { - - String id = ref.getId(); - - if (id == null || "".equals(id)) { - return false; - } - - ChatChannel channel = chatManager.getChatChannel(id); - - if (channel == null) { - throw new IllegalArgumentException("Channel not found"); - } - - if (!chatManager.getCanReadMessage(channel)) { - throw new SecurityException("You do not have permission to access this chat channel"); - } - - Session session = SessionManager.getCurrentSession(); - String sessionId = session.getId(); - - CourierService courier = org.sakaiproject.courier.cover.CourierService.getInstance(); - - ChatRestListener listener = new ChatRestListener(chatManager, courier, sessionId, channel); - - chatManager.addRoomListener(listener, channel.getId()); - - return true; - } - /** * Create a new chat channel for a given context diff --git a/chat/chat-impl/impl/src/java/org/sakaiproject/chat2/model/impl/ChatEntityProducer.java b/chat/chat-impl/impl/src/java/org/sakaiproject/chat2/model/impl/ChatEntityProducer.java index ca00f75ddde4..b50c5477bd93 100644 --- a/chat/chat-impl/impl/src/java/org/sakaiproject/chat2/model/impl/ChatEntityProducer.java +++ b/chat/chat-impl/impl/src/java/org/sakaiproject/chat2/model/impl/ChatEntityProducer.java @@ -27,9 +27,14 @@ import java.io.ByteArrayOutputStream; import java.io.OutputStream; import java.io.OutputStreamWriter; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; +import java.time.format.FormatStyle; import java.util.Collection; import java.util.Iterator; import java.util.List; +import java.util.Locale; import java.util.Map; import java.util.Properties; import java.util.Set; @@ -58,8 +63,8 @@ import org.sakaiproject.site.api.Site; import org.sakaiproject.site.api.ToolConfiguration; import org.sakaiproject.site.cover.SiteService; -import org.sakaiproject.time.cover.TimeService; import org.sakaiproject.user.cover.UserDirectoryService; +import org.sakaiproject.util.ResourceLoader; import org.sakaiproject.util.StringUtil; import org.sakaiproject.util.Web; import org.w3c.dom.DOMException; @@ -394,8 +399,11 @@ public void handleAccess(HttpServletRequest req, HttpServletResponse res, Refere ChatMessage message = getMessage(ref); String title = ref.getDescription(); //MessageHeader messageHead = message.getHeader(); - //String date = messageHead.getDate().toStringLocalFullZ(); - String date = TimeService.newTime(message.getMessageDate().getTime()).toStringLocalFullZ(); + ResourceLoader rl = new ResourceLoader(); + Locale locale = rl.getLocale(); + ZonedDateTime ldt = ZonedDateTime.ofInstant(message.getMessageDate().toInstant(), ZoneId.of(chatManager.getUserTimeZone())); + String date = ldt.format(DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM, FormatStyle.LONG).withLocale(locale)); + String from = UserDirectoryService.getUser(message.getOwner()).getDisplayName(); //String from = messageHead.getFrom().getDisplayName(); String groups = ""; diff --git a/chat/chat-impl/impl/src/java/org/sakaiproject/chat2/model/impl/ChatManagerImpl.java b/chat/chat-impl/impl/src/java/org/sakaiproject/chat2/model/impl/ChatManagerImpl.java index c8018c90648e..7f4f5222c00e 100644 --- a/chat/chat-impl/impl/src/java/org/sakaiproject/chat2/model/impl/ChatManagerImpl.java +++ b/chat/chat-impl/impl/src/java/org/sakaiproject/chat2/model/impl/ChatManagerImpl.java @@ -21,8 +21,17 @@ package org.sakaiproject.chat2.model.impl; +import java.io.File; +import java.io.InputStream; +import java.io.OutputStream; +import java.lang.management.ManagementFactory; +import java.net.URL; import java.sql.Connection; import java.sql.PreparedStatement; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; +import java.time.format.FormatStyle; import java.util.ArrayList; import java.util.Calendar; import java.util.Collections; @@ -30,11 +39,15 @@ import java.util.Date; import java.util.Iterator; import java.util.List; +import java.util.Locale; import java.util.Map; import java.util.HashMap; -import java.util.Observable; -import java.util.Observer; +import java.util.HashSet; +import java.util.Set; +import java.util.TimeZone; +import java.util.concurrent.TimeUnit; +import org.apache.commons.lang.StringUtils; import org.apache.commons.lang.time.DateUtils; import org.hibernate.Query; import org.slf4j.Logger; @@ -44,52 +57,101 @@ import org.hibernate.criterion.Expression; import org.hibernate.criterion.Order; import org.hibernate.criterion.Projections; -import org.sakaiproject.authz.cover.FunctionManager; -import org.sakaiproject.authz.cover.SecurityService; +import org.jgroups.Address; +import org.jgroups.Channel; +import org.jgroups.JChannel; +import org.jgroups.Message; +import org.jgroups.Receiver; +import org.jgroups.View; +import org.jgroups.jmx.JmxConfigurator; +import org.sakaiproject.authz.api.FunctionManager; +import org.sakaiproject.authz.api.SecurityService; import org.sakaiproject.chat2.model.ChatChannel; import org.sakaiproject.chat2.model.ChatManager; -import org.sakaiproject.chat2.model.RoomObserver; +import org.sakaiproject.chat2.model.SimpleUser; +import org.sakaiproject.chat2.model.TransferableChatMessage; +import org.sakaiproject.chat2.model.TransferableChatMessage.MessageType; import org.sakaiproject.chat2.model.ChatFunctions; import org.sakaiproject.chat2.model.ChatMessage; -import org.sakaiproject.component.cover.ServerConfigurationService; -import org.sakaiproject.entity.api.EntityManager; +import org.sakaiproject.chat2.model.DeleteMessage; +import org.sakaiproject.chat2.model.MessageDateString; +import org.sakaiproject.component.api.ServerConfigurationService; import org.sakaiproject.entity.api.Reference; +import org.sakaiproject.entity.api.ResourceProperties; import org.sakaiproject.entity.api.Summary; import org.sakaiproject.event.api.Event; -import org.sakaiproject.event.cover.EventTrackingService; +import org.sakaiproject.event.api.EventTrackingService; +import org.sakaiproject.event.api.UsageSession; +import org.sakaiproject.event.api.UsageSessionService; import org.sakaiproject.exception.IdInvalidException; import org.sakaiproject.exception.IdUsedException; import org.sakaiproject.exception.PermissionException; - -import org.sakaiproject.util.FormattedText; -import org.sakaiproject.site.cover.SiteService; -import org.sakaiproject.time.api.Time; -import org.sakaiproject.time.cover.TimeService; -import org.sakaiproject.tool.cover.SessionManager; +import org.sakaiproject.presence.api.PresenceService; +import org.sakaiproject.site.api.SiteService; +import org.sakaiproject.time.api.TimeService; +import org.sakaiproject.tool.api.SessionManager; +import org.sakaiproject.user.api.Preferences; +import org.sakaiproject.user.api.PreferencesService; import org.sakaiproject.user.api.User; +import org.sakaiproject.user.api.UserDirectoryService; import org.sakaiproject.user.api.UserNotDefinedException; -import org.sakaiproject.user.cover.UserDirectoryService; +import org.sakaiproject.util.ResourceLoader; +import org.sakaiproject.util.api.FormattedText; import org.springframework.orm.hibernate4.support.HibernateDaoSupport; import org.springframework.transaction.support.TransactionSynchronizationAdapter; import org.springframework.transaction.support.TransactionSynchronizationManager; +import com.google.common.cache.Cache; +import com.google.common.cache.CacheBuilder; +import com.google.common.cache.CacheLoader; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.Getter; +import lombok.Setter; + /** * * @author andersjb * */ -public class ChatManagerImpl extends HibernateDaoSupport implements ChatManager, Observer { +public class ChatManagerImpl extends HibernateDaoSupport implements ChatManager, Receiver { private int messagesMax = 100; protected final transient Logger logger = LoggerFactory.getLogger(getClass()); - /** the clients listening to the various rooms */ - protected Map> roomListeners = new HashMap>(); + @Getter @Setter private ChatChannel defaultChannelSettings; + + @Setter private UserDirectoryService userDirectoryService; + @Setter private ServerConfigurationService serverConfigurationService; + @Setter private PresenceService presenceService; + @Setter private SessionManager sessionManager; + @Setter private UsageSessionService usageSessionService; + @Setter private FormattedText formattedText; + @Setter private PreferencesService preferencesService; + @Setter private SecurityService securityService; + @Setter private FunctionManager functionManager; + @Setter private SiteService siteService; + @Setter private EventTrackingService eventTrackingService; + + /** MAP[SESSION_KEY][CHANNEL_ID] -> List */ + private Cache>> messageMap; + + /** MAP[CHANNEL_ID][SESSION_ID] -> TransferableChatMessage */ + /** We store the session_id to allow login multiple times with different browsers */ + private Cache> heartbeatMap; + + // Used for fetching user's default language locale + ResourceLoader rl = new ResourceLoader(); + //stores users timezone + private Cache timezoneCache; - private EntityManager entityManager; + @Getter private int pollInterval = 5000; //5 sec - private ChatChannel defaultChannelSettings; + /* JGroups channel for keeping the above maps in sync across nodes in a Sakai cluster */ + private JChannel clusterChannel = null; + private boolean clustered = false; static Comparator channelComparatorAsc = new Comparator() { @@ -123,17 +185,71 @@ protected void init() throws Exception try { - EventTrackingService.addObserver(this); - // register functions - if(FunctionManager.getRegisteredFunctions(ChatFunctions.CHAT_FUNCTION_PREFIX).size() == 0) { - FunctionManager.registerFunction(ChatFunctions.CHAT_FUNCTION_READ); - FunctionManager.registerFunction(ChatFunctions.CHAT_FUNCTION_NEW); - FunctionManager.registerFunction(ChatFunctions.CHAT_FUNCTION_DELETE_ANY); - FunctionManager.registerFunction(ChatFunctions.CHAT_FUNCTION_DELETE_OWN); - FunctionManager.registerFunction(ChatFunctions.CHAT_FUNCTION_DELETE_CHANNEL); - FunctionManager.registerFunction(ChatFunctions.CHAT_FUNCTION_NEW_CHANNEL); - FunctionManager.registerFunction(ChatFunctions.CHAT_FUNCTION_EDIT_CHANNEL); + if(functionManager.getRegisteredFunctions(ChatFunctions.CHAT_FUNCTION_PREFIX).size() == 0) { + functionManager.registerFunction(ChatFunctions.CHAT_FUNCTION_READ); + functionManager.registerFunction(ChatFunctions.CHAT_FUNCTION_NEW); + functionManager.registerFunction(ChatFunctions.CHAT_FUNCTION_DELETE_ANY); + functionManager.registerFunction(ChatFunctions.CHAT_FUNCTION_DELETE_OWN); + functionManager.registerFunction(ChatFunctions.CHAT_FUNCTION_DELETE_CHANNEL); + functionManager.registerFunction(ChatFunctions.CHAT_FUNCTION_NEW_CHANNEL); + functionManager.registerFunction(ChatFunctions.CHAT_FUNCTION_EDIT_CHANNEL); + } + + pollInterval = serverConfigurationService.getInt("chat.pollInterval", 5000); + + messageMap = CacheBuilder.newBuilder() + //.recordStats() + .expireAfterAccess(5, TimeUnit.MINUTES) + .build(); + heartbeatMap = CacheBuilder.newBuilder() + //.recordStats() + .expireAfterAccess(pollInterval*2, TimeUnit.MILLISECONDS) + .build(); + + timezoneCache = CacheBuilder.newBuilder() + .maximumSize(1000) + .expireAfterWrite(600, TimeUnit.SECONDS) + .build(); + + try { + String channelId = serverConfigurationService.getString("chat.cluster.channel", ""); + + if (StringUtils.isNotBlank(channelId)) { + URL jgroupsConfigURL = null; + // Pick up the config file from sakai home if it exists + File jgroupsConfig = new File(serverConfigurationService.getSakaiHomePath() + File.separator + "jgroups-chat-config.xml"); + if (jgroupsConfig.exists()) { + logger.debug("Using custom jgroups config file: {}", jgroupsConfig.getAbsolutePath()); + clusterChannel = new JChannel(jgroupsConfig); + } else if((jgroupsConfigURL = this.getClass().getClassLoader().getResource("jgroups-config.xml")) != null) { //pick up our default file + logger.debug("Using default jgroups config file: {}", jgroupsConfigURL); + clusterChannel = new JChannel(jgroupsConfigURL); + } else { + logger.debug("No jgroups config file. Using jgroup defaults."); + clusterChannel = new JChannel(); + } + + logger.debug("JGROUPS PROTOCOL: {}", clusterChannel.getProtocolStack().printProtocolSpecAsXML()); + + clusterChannel.setReceiver(this); + clusterChannel.connect(channelId); + // We don't want a copy of our JGroups messages sent back to us + clusterChannel.setDiscardOwnMessages(true); + //JmxConfigurator.registerChannel(clusterChannel, ManagementFactory.getPlatformMBeanServer(), "DefaultDomain:name=JGroups"); + clustered = true; + + logger.info("Chat is connected on JGroups channel '" + channelId + "'"); + } else { + logger.info("No 'chat.cluster.channel' specified in sakai.properties. JGroups will not be used and chat messages will not be replicated."); + } + } catch (Exception e) { + logger.error("Error creating JGroups channel. Chat messages will now NOT BE KEPT IN SYNC", e); + + if (clusterChannel != null && clusterChannel.isConnected()) { + // This calls disconnect() first + clusterChannel.close(); + } } } @@ -148,7 +264,10 @@ protected void init() throws Exception */ public void destroy() { - EventTrackingService.deleteObserver(this); + if (clusterChannel != null && clusterChannel.isConnected()) { + // This calls disconnect() first + clusterChannel.close(); + } logger.info("destroy()"); } @@ -428,7 +547,7 @@ public void migrateMessage(String sql, Object[] values) { message.setChatChannel(channel); message.setOwner(owner); message.setMessageDate(messageDate); - message.setBody(FormattedText.convertPlaintextToFormattedText(body)); + message.setBody(formattedText.convertPlaintextToFormattedText(body)); message.setMigratedMessageId(migratedId); @@ -457,8 +576,8 @@ public boolean getCanDelete(ChatMessage message) { boolean canDeleteAny = can(ChatFunctions.CHAT_FUNCTION_DELETE_ANY, context); boolean canDeleteOwn = can(ChatFunctions.CHAT_FUNCTION_DELETE_OWN, context); - boolean isOwner = SessionManager.getCurrentSessionUserId() != null ? - SessionManager.getCurrentSessionUserId().equals(message.getOwner()) : false; + boolean isOwner = sessionManager.getCurrentSessionUserId() != null ? + sessionManager.getCurrentSessionUserId().equals(message.getOwner()) : false; boolean canDelete = canDeleteAny; @@ -513,7 +632,7 @@ public boolean getCanReadMessage(ChatChannel channel) public boolean getCanPostMessage(ChatChannel channel) { // We don't currently support posting messages by anonymous users - if (SessionManager.getCurrentSessionUserId() == null) + if (sessionManager.getCurrentSessionUserId() == null) return false; boolean allowed = false; @@ -655,57 +774,6 @@ public ChatChannel getDefaultChannel(String contextId, String placement) { return null; } - /** - * {@inheritDoc} - */ - public void addRoomListener(RoomObserver observer, String roomId) - { - List roomObservers; - synchronized(roomListeners) { - if(roomListeners.get(roomId) == null) - roomListeners.put(roomId, new ArrayList()); - roomObservers = roomListeners.get(roomId); - } - synchronized(roomObservers) { - roomObservers.add(observer); - } - - if (logger.isDebugEnabled()) { - logger.debug("after add roomObservers " + roomObservers); - } - - } - - /** - * {@inheritDoc} - */ - public void removeRoomListener(RoomObserver observer, String roomId) - { - - if(roomListeners.get(roomId) != null) { - List roomObservers = roomListeners.get(roomId); - - if(roomObservers != null) { - synchronized(roomObservers) { - - roomObservers.remove(observer); - if(roomObservers.size() == 0) { - - synchronized(roomListeners) { - roomListeners.remove(roomId); - } - - } - - } - } // end if(roomObservers != null) - - if (logger.isDebugEnabled()) { - logger.debug("after remove roomObservers " + roomObservers); - } - } - } - /** * {@inheritDoc} */ @@ -714,6 +782,8 @@ public void sendMessage(ChatMessage message) { getHibernateTemplate().flush(); txSync.afterCompletion(ChatMessageTxSync.STATUS_COMMITTED); + + sendToCluster(new TransferableChatMessage(message)); } /** @@ -728,6 +798,8 @@ public void sendDeleteMessage(ChatMessage message) { else { txSync.afterCompletion(ChatMessageDeleteTxSync.STATUS_COMMITTED); } + + sendToCluster(new TransferableChatMessage(TransferableChatMessage.MessageType.REMOVE, message.getId(), message.getChatChannel().getId())); } @@ -757,6 +829,8 @@ public void sendDeleteChannelMessages(ChatChannel channel) { else { txSync.afterCompletion(ChatChannelMessagesDeleteTxSync.STATUS_COMMITTED); } + + sendToCluster(new TransferableChatMessage(TransferableChatMessage.MessageType.REMOVE, "*", channel.getId())); } /** @@ -774,18 +848,20 @@ public ChatMessageDeleteTxSync(ChatMessage message) { public void afterCompletion(int status) { Event event = null; String function = ChatFunctions.CHAT_FUNCTION_DELETE_ANY; - if (message.getOwner().equals(SessionManager.getCurrentSessionUserId())) + if (message.getOwner().equals(sessionManager.getCurrentSessionUserId())) { // own or any function = ChatFunctions.CHAT_FUNCTION_DELETE_OWN; } - event = EventTrackingService.newEvent(function, + event = eventTrackingService.newEvent(function, message.getReference(), false); if (event != null) - EventTrackingService.post(event); + eventTrackingService.post(event); + + addMessageToMap(new TransferableChatMessage(TransferableChatMessage.MessageType.REMOVE, message.getId(), message.getChatChannel().getId())); } } @@ -803,11 +879,14 @@ public ChatMessageTxSync(ChatMessage message) { public void afterCompletion(int status) { Event event = null; - event = EventTrackingService.newEvent(ChatFunctions.CHAT_FUNCTION_NEW, + event = eventTrackingService.newEvent(ChatFunctions.CHAT_FUNCTION_NEW, message.getReference(), false); if (event != null) - EventTrackingService.post(event); + eventTrackingService.post(event); + + addMessageToMap(new TransferableChatMessage(message)); + } } @@ -825,11 +904,11 @@ public ChatChannelDeleteTxSync(ChatChannel channel) { public void afterCompletion(int status) { Event event = null; - event = EventTrackingService.newEvent(ChatFunctions.CHAT_FUNCTION_DELETE_CHANNEL, + event = eventTrackingService.newEvent(ChatFunctions.CHAT_FUNCTION_DELETE_CHANNEL, channel.getReference(), false); if (event != null) - EventTrackingService.post(event); + eventTrackingService.post(event); } } @@ -847,82 +926,13 @@ public ChatChannelMessagesDeleteTxSync(ChatChannel channel) { public void afterCompletion(int status) { Event event = null; - event = EventTrackingService.newEvent(ChatFunctions.CHAT_FUNCTION_DELETE_ANY, + event = eventTrackingService.newEvent(ChatFunctions.CHAT_FUNCTION_DELETE_ANY, channel.getReference(), false); if (event != null) - EventTrackingService.post(event); - } - } + eventTrackingService.post(event); - /** - * This method is called whenever the observed object is changed. An - * application calls an Observable object's - * notifyObservers method to have all the object's - * observers notified of the change. - * - * This operates within its own Thread so normal rules and conditions don't apply - * - * @param o the observable object. - * @param arg an argument passed to the notifyObservers - * method. - */ - @SuppressWarnings("unchecked") - public void update(Observable o, Object arg) { - if (arg instanceof Event) { - Event event = (Event)arg; - - Reference ref = getEntityManager().newReference(event.getResource()); - - if (event.getEvent().equals(ChatFunctions.CHAT_FUNCTION_NEW)) { - - // get the actual message and distribute it. Otherwise each - // observer will fetch their own copy of the message. - - String id = ref.getId(); - if (id == null) - return; - ChatMessage message = getMessage(ref.getId()); - if (message == null) - return; - - //String[] messageParams = event.getResource().split(":"); - - ArrayList observers = (ArrayList) roomListeners.get(ref.getContainer()); - - // originally we did the iteration inside synchronized. - // however that turns out to hold the lock too long - // a shallow copy of an arraylist shouldn't be bad. - // we currently call removeRoom from receivedMessage in - // some cases, so it can't be locked or we will deadlock - if(observers != null) { - synchronized(observers) { - observers = (ArrayList)observers.clone(); - } - for(Iterator i = observers.iterator(); i.hasNext(); ) { - RoomObserver observer = i.next(); - - observer.receivedMessage(ref.getContainer(), message); - } - } - - - } else if (event.getEvent().equals(ChatFunctions.CHAT_FUNCTION_DELETE_CHANNEL)) { - //String chatChannelId = event.getResource(); - - ArrayList observers = (ArrayList) roomListeners.get(ref.getId()); - - if(observers != null) { - synchronized(observers) { - observers = (ArrayList)observers.clone(); - } - for(Iterator i = observers.iterator(); i.hasNext(); ) { - RoomObserver observer = i.next(); - - observer.roomDeleted(ref.getId()); - } - } - } + addMessageToMap(new TransferableChatMessage(TransferableChatMessage.MessageType.REMOVE, "*", channel.getId())); } } @@ -967,22 +977,22 @@ public void makeDefaultContextChannel(ChatChannel channel, String placement) { protected void checkPermission(String function, String context) throws PermissionException { - if (!SecurityService.unlock(function, SiteService.siteReference(context))) + if (!securityService.unlock(function, siteService.siteReference(context))) { - String user = SessionManager.getCurrentSessionUserId(); + String user = sessionManager.getCurrentSessionUserId(); throw new PermissionException(user, function, context); } } protected boolean can(String function, String context) { - return SecurityService.unlock(function, SiteService.siteReference(context)); + return securityService.unlock(function, siteService.siteReference(context)); } /** * {@inheritDoc} */ public boolean isMaintainer(String context) { - return SecurityService.unlock(SiteService.SECURE_UPDATE_SITE, SiteService.siteReference(context)); + return securityService.unlock(SiteService.SECURE_UPDATE_SITE, siteService.siteReference(context)); } @@ -990,9 +1000,12 @@ protected String getSummaryFromHeader(ChatMessage item) throws UserNotDefinedExc { String body = item.getBody(); if ( body.length() > 50 ) body = body.substring(1,49); - User user = UserDirectoryService.getUser(item.getOwner()); - Time messageTime = TimeService.newTime(item.getMessageDate().getTime()); - String newText = body + ", " + user.getDisplayName() + ", " + messageTime.toStringLocalFull(); + User user = userDirectoryService.getUser(item.getOwner()); + + ZonedDateTime ldt = ZonedDateTime.ofInstant(item.getMessageDate().toInstant(), ZoneId.of(getUserTimeZone())); + Locale locale = rl.getLocale(); + + String newText = body + ", " + user.getDisplayName() + ", " + ldt.format(DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM, FormatStyle.SHORT).withLocale(locale)); return newText; } @@ -1008,14 +1021,16 @@ public Map getSummary(String channel, int items, int days) List messages = getChannelMessages(getChatChannel(channel), new Date(startTime), 0, items, true); Iterator iMsg = messages.iterator(); - Time pubDate = null; + ZonedDateTime pubDate = null; String summaryText = null; Map m = new HashMap(); + Locale locale = rl.getLocale(); + DateTimeFormatter dtf = DateTimeFormatter.ofPattern("EEE, dd MMM yyyy HH:mm:ss Z").withLocale(locale); while (iMsg.hasNext()) { ChatMessage item = iMsg.next(); //MessageHeader header = item.getHeader(); - Time newTime = TimeService.newTime(item.getMessageDate().getTime()); - if ( pubDate == null || newTime.before(pubDate) ) pubDate = newTime; + ZonedDateTime newTime = ZonedDateTime.ofInstant(item.getMessageDate().toInstant(), ZoneId.of(getUserTimeZone())); + if ( pubDate == null || newTime.isBefore(pubDate) ) pubDate = newTime; try { String newText = getSummaryFromHeader(item); if ( summaryText == null ) { @@ -1029,7 +1044,7 @@ public Map getSummary(String channel, int items, int days) } } if ( pubDate != null ) { - m.put(Summary.PROP_PUBDATE, pubDate.toStringRFC822Local()); + m.put(Summary.PROP_PUBDATE, pubDate.format(dtf)); } if ( summaryText != null ) { m.put(Summary.PROP_DESCRIPTION, summaryText); @@ -1038,21 +1053,6 @@ public Map getSummary(String channel, int items, int days) return null; } - - /** - * @return the entityManager - */ - public EntityManager getEntityManager() { - return entityManager; - } - - /** - * @param entityManager the entityManager to set - */ - public void setEntityManager(EntityManager entityManager) { - this.entityManager = entityManager; - } - /** * Access the partial URL that forms the root of resource URLs. * @@ -1062,7 +1062,7 @@ public void setEntityManager(EntityManager entityManager) { */ protected String getAccessPoint(boolean relative) { - return (relative ? "" : ServerConfigurationService.getAccessUrl()) + REFERENCE_ROOT; + return (relative ? "" : serverConfigurationService.getAccessUrl()) + REFERENCE_ROOT; } // getAccessPoint @@ -1089,25 +1089,342 @@ public String getLabel() { return CHAT; } + public void setMessagesMax(int messagesMax) { + this.messagesMax = messagesMax; + } + + public int getMessagesMax() { + return messagesMax; + } + + //******************************************************************** /** * {@inheritDoc} */ - public ChatChannel getDefaultChannelSettings() { - return defaultChannelSettings; + public List getPresentUsers(String siteId, String channelId){ + Set presentUsers = new HashSet(); + + if (StringUtils.isNotBlank(siteId)) { + + // refresh our presence at the location and retrieve the present users + String location = siteId + "-presence"; + presenceService.setPresence(location); + + for(UsageSession us : presenceService.getPresence(siteId + "-presence")){ + //check if still online in the heartbeat map + if (isOnline(channelId, us.getId())) { + TransferableChatMessage tcm = heartbeatMap.getIfPresent(channelId).getIfPresent(us.getId()); + String sessionUserId = getUserIdFromSessionKey(tcm.getId()); + + String displayName = us.getUserDisplayId(); + String userId = us.getUserId(); + try { + displayName = userDirectoryService.getUser(us.getUserId()).getDisplayName(); + //if user stored in heartbeat is different to the presence one + if(!userId.equals(sessionUserId)) { + userId += ":"+sessionUserId; + displayName += " (" + userDirectoryService.getUser(sessionUserId).getDisplayName() + ")"; + } + }catch(Exception e){ + logger.error("Error getting user "+sessionUserId, e); + } + + presentUsers.add(new SimpleUser(userId, formattedText.escapeHtml(displayName, true))); + } + else { + logger.debug("Heartbeat not found for sessionId {}, so not adding to presentUsers", us.getId()); + } + } + + } + List ret = new ArrayList<>(presentUsers); + ret.sort((a, b) -> a.getName().compareToIgnoreCase(b.getName())); + return ret; } /** * {@inheritDoc} */ - public void setDefaultChannelSettings(ChatChannel defaultChannelSettings) { - this.defaultChannelSettings = defaultChannelSettings; + public Map handleChatData(String siteId, String channelId, String sessionKey) { + + Map data = new HashMap(); + + if(StringUtils.isNotBlank(sessionKey)) { + //current user is requesting data -> is online in that channel + TransferableChatMessage hb = addHeartBeat(channelId, sessionKey); + + sendToCluster(hb); + + List presentUsers = getPresentUsers(siteId, channelId); + + ChatChannel channel = null; + + if (channelId != null) { + channel = getChatChannel(channelId); + } else if (siteId != null) { + channel = getDefaultChannel(siteId, null); } - public void setMessagesMax(int messagesMax) { - this.messagesMax = messagesMax; + List messages = new ArrayList(); + List delete = new ArrayList(); + //as guava cache is synchronized, maybe this is not necessary + synchronized (messageMap){ + if (messageMap.getIfPresent(sessionKey) != null) { + try { + if(messageMap.getIfPresent(sessionKey).get(channelId) != null) { + for(TransferableChatMessage tcm : messageMap.getIfPresent(sessionKey).get(channelId)){ + switch(tcm.getType()){ + case CHAT: + messages.add(tcm.toChatMessage(channel)); + break; + case REMOVE: + delete.add(new DeleteMessage(tcm.getId(), tcm.getChannelId())); + break; + } + } + } + } catch(Exception e){ + logger.error("Error getting messages in channel "+channelId+" for session_key "+sessionKey, e); } - public int getMessagesMax() { - return messagesMax; + //clear all messages for this user + messageMap.invalidate(sessionKey); + } + } + //sort messages by date + messages.sort((a, b) -> a.getMessageDate().compareTo(b.getMessageDate())); + + //send clear message to jGroups + sendToCluster(new TransferableChatMessage(TransferableChatMessage.MessageType.CLEAR, sessionKey)); + + data.put("messages", messages); + data.put("deletedMessages", delete); + data.put("presentUsers", presentUsers); + } + + return data; + } + + /** + * {@inheritDoc} + */ + public int getPollInterval(){ + return pollInterval; + } + + + /** + * {@inheritDoc} + */ + public String getSessionKey(){ + try { + UsageSession usageSession = usageSessionService.getSession(); + String sessionId = usageSession.getId(); + //this is different from usageSession.getUserId(), because we want to know both users (real and login as) + String sessionUser = sessionManager.getCurrentSessionUserId(); + + return sessionId+":"+sessionUser; + } catch(Exception e){ + logger.error("Error getting current session key", e); + } + return null; + } + + /** + * {@inheritDoc} + */ + public MessageDateString getMessageDateString(ChatMessage msg){ + ZonedDateTime ldt = ZonedDateTime.ofInstant(msg.getMessageDate().toInstant(), ZoneId.of(getUserTimeZone())); + + Locale locale = rl.getLocale(); + + DateTimeFormatter dtf = DateTimeFormatter.ofLocalizedDate(FormatStyle.MEDIUM).withLocale(locale); + DateTimeFormatter dtf2 = DateTimeFormatter.ofLocalizedTime(FormatStyle.LONG).withLocale(locale); + DateTimeFormatter dtf3 = DateTimeFormatter.ofPattern("yyyyMMddHHmmssSSS", locale); + + return new MessageDateString(ldt.format(dtf), ldt.format(dtf2), ldt.format(dtf3)); + } + + /** + * {@inheritDoc} + */ + public String getUserTimeZone() { + String userId = sessionManager.getCurrentSessionUserId(); + if (userId == null) { return TimeZone.getDefault().getID(); } + + String elementCacheId = "TZ_"+userId; + String element = timezoneCache.getIfPresent(elementCacheId); + if(element != null) { + return element; + } + + Preferences prefs = preferencesService.getPreferences(userId); + ResourceProperties tzProps = prefs.getProperties(TimeService.APPLICATION_ID); + String timeZone = tzProps.getProperty(TimeService.TIMEZONE_KEY); + + if(StringUtils.isBlank(timeZone)) { + timeZone = TimeZone.getDefault().getID(); + } + + timezoneCache.put(elementCacheId, timeZone); + + return timeZone; + } + + + /** + * JGroups message listener. + */ + public void receive(Message msg) { + Object o = msg.getObject(); + if (o instanceof TransferableChatMessage) { + TransferableChatMessage message = (TransferableChatMessage) o; + + String id = message.getId(); + String channelId = message.getChannelId(); + + switch(message.getType()){ + case CHAT : + logger.debug("Received message {} from cluster ...", id); + addMessageToMap(message); + break; + case HEARTBEAT : + logger.debug("Received heartbeat {} - {} from cluster ...", id, channelId); + addHeartBeat(channelId, id); + break; + case CLEAR : + logger.debug("Received clear message {} from cluster ...", id); + //as guava cache is synchronized, maybe this is not necessary + synchronized (messageMap){ + messageMap.invalidate(id); + } + break; + case REMOVE : + logger.debug("Received remove message {} from cluster ...", id); + addMessageToMap(message); + break; + } + } + } + + //******************************************************************** + // private utility functions + + /** + * Implements a threadsafe addition to the message map + */ + private void addMessageToMap(TransferableChatMessage msg) { + String channelId = msg.getChannelId(); + //as guava cache is synchronized, maybe this is not necessary + synchronized (messageMap){ + //get all users (sessions) present in the channel where the message goes to + Cache sessionsInChannel = heartbeatMap.getIfPresent(channelId); + if(sessionsInChannel != null) { + for(String sessionId : sessionsInChannel.asMap().keySet()) { + TransferableChatMessage tcm = sessionsInChannel.getIfPresent(sessionId); + String sessionKey = tcm.getId(); + try { + Map> channelMap = messageMap.get(sessionKey, () -> { + return new HashMap>(); + }); + + if(channelMap.get(channelId) == null) { + channelMap.put(channelId, new ArrayList()); + } + channelMap.get(channelId).add(msg); + + logger.debug("Added chat message to channel={}, sessionKey={}", channelId, sessionKey); + } catch(Exception e){ + logger.warn("Failed to add chat message to channel={}, sessionKey={}", channelId, sessionKey); + } + } + } + } + } + + /** + * Set/Update the heartbeat for given sessionKey (indexed by channelId and sessionId) + * @param channelId + * @param sessionKey + * @return + */ + private TransferableChatMessage addHeartBeat(String channelId, String sessionKey){ + TransferableChatMessage ret = null; + + String sessionId = getSessionIdFromSessionKey(sessionKey); + + try { + ret = TransferableChatMessage.HeartBeat(channelId, sessionKey); + heartbeatMap.get(channelId, () -> { + return CacheBuilder.newBuilder() + .expireAfterWrite(pollInterval*2, TimeUnit.MILLISECONDS) + .build(); + }).put(sessionId, ret); + } catch(Exception e){ + logger.error("Error adding heartbet in channel : "+channelId+" and session_key : "+sessionKey); + } + return ret; + } + + /** Check if given userId is online in the channel. + * + * @param channelId + * @param userId + * @return + */ + private boolean isOnline(String channelId, String sessionId) { + if(heartbeatMap.getIfPresent(channelId) == null) { + return false; + } + + //thanks to the cache auto-expiration system, not updated hearbeats will be automatically removed + return (heartbeatMap.getIfPresent(channelId).getIfPresent(sessionId) != null); + } + + private void sendToCluster(TransferableChatMessage message){ + if (clustered) { + try { + logger.debug("Sending message ({}) id:{}, channelId:{} to cluster ...", message.getType(), message.getId(), message.getChannelId()); + Message msg = new Message(null, message); + clusterChannel.send(msg); + } catch (Exception e) { + logger.error("Error sending JGroups message", e); + } + } + } + + + private String getSessionIdFromSessionKey(String sessionKey){ + return sessionKey.substring(0, sessionKey.indexOf(":")); + } + private String getUserIdFromSessionKey(String sessionKey){ + return sessionKey.substring(sessionKey.indexOf(":") + 1); + } + + //******************************************************************** + // jGroups override functions + + @Override + public void getState(OutputStream output) throws Exception { + } + + @Override + public void setState(InputStream input) throws Exception { + } + + @Override + public void block() { + } + + @Override + public void suspect(Address arg0) { + } + + @Override + public void unblock() { + } + + @Override + public void viewAccepted(View arg0) { } } diff --git a/chat/chat-impl/impl/src/java/org/sakaiproject/chat2/model/impl/ChatMessageEntityProvider.java b/chat/chat-impl/impl/src/java/org/sakaiproject/chat2/model/impl/ChatMessageEntityProvider.java index fbd106db0741..93dafa88593d 100644 --- a/chat/chat-impl/impl/src/java/org/sakaiproject/chat2/model/impl/ChatMessageEntityProvider.java +++ b/chat/chat-impl/impl/src/java/org/sakaiproject/chat2/model/impl/ChatMessageEntityProvider.java @@ -22,20 +22,31 @@ package org.sakaiproject.chat2.model.impl; import java.util.Date; +import java.util.HashMap; import java.util.ArrayList; import java.util.List; import java.util.Map; import org.slf4j.Logger; import org.slf4j.LoggerFactory; + +import lombok.Data; +import lombok.Setter; + +import org.apache.commons.lang.StringUtils; import org.sakaiproject.chat2.model.ChatChannel; import org.sakaiproject.chat2.model.ChatManager; import org.sakaiproject.chat2.model.ChatMessage; +import org.sakaiproject.chat2.model.MessageDateString; import org.sakaiproject.entitybroker.EntityReference; +import org.sakaiproject.entitybroker.EntityView; import org.sakaiproject.entitybroker.entityprovider.CoreEntityProvider; +import org.sakaiproject.entitybroker.entityprovider.annotations.EntityCustomAction; +import org.sakaiproject.entitybroker.entityprovider.capabilities.ActionsExecutable; import org.sakaiproject.entitybroker.entityprovider.capabilities.AutoRegisterEntityProvider; import org.sakaiproject.entitybroker.entityprovider.capabilities.CollectionResolvable; import org.sakaiproject.entitybroker.entityprovider.capabilities.Createable; +import org.sakaiproject.entitybroker.entityprovider.capabilities.Deleteable; import org.sakaiproject.entitybroker.entityprovider.capabilities.Describeable; import org.sakaiproject.entitybroker.entityprovider.capabilities.Inputable; import org.sakaiproject.entitybroker.entityprovider.capabilities.Outputable; @@ -44,17 +55,21 @@ import org.sakaiproject.entitybroker.entityprovider.search.Restriction; import org.sakaiproject.entitybroker.entityprovider.search.Search; import org.sakaiproject.exception.PermissionException; -import org.sakaiproject.tool.cover.SessionManager; +import org.sakaiproject.tool.api.SessionManager; import org.sakaiproject.user.api.User; +import org.sakaiproject.user.api.UserDirectoryService; import org.sakaiproject.user.api.UserNotDefinedException; -import org.sakaiproject.user.cover.UserDirectoryService; -import org.sakaiproject.util.FormattedText; +import org.sakaiproject.util.api.FormattedText; public class ChatMessageEntityProvider implements CoreEntityProvider, AutoRegisterEntityProvider, Outputable, Inputable, Resolvable, - Describeable, Createable, CollectionResolvable { + Describeable, Createable, Deleteable, CollectionResolvable, ActionsExecutable { private ChatManager chatManager; + + @Setter private UserDirectoryService userDirectoryService; + @Setter private SessionManager sessionManager; + @Setter private FormattedText formattedText; public final static String ENTITY_PREFIX = "chat-message"; @@ -62,7 +77,7 @@ public class ChatMessageEntityProvider implements CoreEntityProvider, // We use a custom object here to avoid side-effects of EB setting the body value for new messages, // and avoid returning unwanted fields for getting messages. - + @Data public class SimpleChatMessage { private String id; @@ -73,6 +88,8 @@ public class SimpleChatMessage { private String ownerDisplayName; private Date messageDate; private String body; + private boolean removeable; + private MessageDateString messageDateString; public SimpleChatMessage() { } @@ -87,66 +104,15 @@ public SimpleChatMessage(ChatMessage msg) this.context = msg.getChatChannel().getContext(); try { - User msgowner = UserDirectoryService.getUser(this.owner); + User msgowner = userDirectoryService.getUser(this.owner); this.ownerDisplayId = msgowner.getDisplayId(); this.ownerDisplayName = msgowner.getDisplayName(); } catch (UserNotDefinedException e) { // user not found - ignore } + + removeable = chatManager.getCanDelete(msg); } - - public String getBody() { - return body; - } - - public void setBody(String body) { - this.body = body; - } - - public String getChatChannelId() { - return chatChannelId; - } - - public void setChatChannelId(String chatChannelId) { - this.chatChannelId = chatChannelId; - } - - public String getId() { - return id; - } - - public Date getMessageDate() { - return messageDate; - } - - public String getContext() { - return context; - } - - public void setContext(String context) { - this.context = context; - } - - public String getOwner() { - return owner; - } - - public void setOwnerDisplayId(String ownerDisplayId) { - this.ownerDisplayId = ownerDisplayId; - } - - public String getOwnerDisplayId() { - return ownerDisplayId; - } - - public void setOwnerDisplayName(String ownerDisplayName) { - this.ownerDisplayName = ownerDisplayName; - } - - public String getOwnerDisplayName() { - return ownerDisplayName; - } - } public boolean entityExists(String id) { @@ -235,12 +201,12 @@ public String createEntity(EntityReference ref, Object entity, try { message = getChatManager().createNewMessage(channel, - SessionManager.getCurrentSessionUserId()); + sessionManager.getCurrentSessionUserId()); } catch (PermissionException e) { throw new SecurityException("No permission to post in this channel"); } - message.setBody(FormattedText.convertPlaintextToFormattedText(inmsg.getBody())); + message.setBody(formattedText.convertPlaintextToFormattedText(inmsg.getBody())); chatManager.updateMessage(message); chatManager.sendMessage(message); @@ -322,5 +288,70 @@ public List getEntities(EntityReference ref, Search search) { return msglist; } + + public void deleteEntity(EntityReference ref, Map params){ + + String id = ref.getId(); + + ChatMessage msg = chatManager.getMessage(id); + + if (msg == null) { + throw new IllegalArgumentException("Invalid message id"); + } + + try { + chatManager.deleteMessage(msg); + }catch(Exception e){ + throw new SecurityException("No permission to remove this message"); + } + + } + + /** + * The JS client calls this to grab the latest data in one call. Latest messages, online users and removed messages + * (in a channel) are all returned in one lump of JSON. Also is used to indicate that current user is alive (updating his heartbeat). + */ + @EntityCustomAction(action = "chatData", viewKey = EntityView.VIEW_SHOW) + public Map handleChatData(EntityReference ref, Map params) { + User currentUser = userDirectoryService.getCurrentUser(); + User anon = userDirectoryService.getAnonymousUser(); + + if (anon.equals(currentUser)) { + LOG.debug("No current user"); + throw new SecurityException("You must be logged in to use this service"); + } + + String siteId = (String) params.get("siteId"); + if (StringUtils.isBlank(siteId)) { + LOG.debug("No siteId specified"); + throw new SecurityException("You must be specify the site ID"); + } + LOG.debug("siteId: {}", siteId); + + String channelId = (String) params.get("channelId"); + if (StringUtils.isBlank(channelId)) { + LOG.debug("No channelId specified"); + throw new SecurityException("You must be specify the channel ID"); + } + LOG.debug("channelId: {}", channelId); + + ChatChannel channel = chatManager.getChatChannel(channelId); + if (!chatManager.getCanReadMessage(channel)) { + throw new SecurityException("You do not have permission to access this channel"); + } + + Map data = chatManager.handleChatData(siteId, channelId, chatManager.getSessionKey()); + if(data.get("messages") != null){ + List messages = (List)data.get("messages"); + List msglist = new ArrayList(); + for(ChatMessage msg : messages){ + SimpleChatMessage s_msg = new SimpleChatMessage(msg); + s_msg.setMessageDateString(chatManager.getMessageDateString(msg)); + msglist.add(s_msg); + } + data.put("messages", msglist); + } + return data; + } } diff --git a/chat/chat-impl/impl/src/java/org/sakaiproject/chat2/model/impl/ChatRestDelivery.java b/chat/chat-impl/impl/src/java/org/sakaiproject/chat2/model/impl/ChatRestDelivery.java deleted file mode 100644 index 56d575dca85d..000000000000 --- a/chat/chat-impl/impl/src/java/org/sakaiproject/chat2/model/impl/ChatRestDelivery.java +++ /dev/null @@ -1,206 +0,0 @@ -/********************************************************************************** - * $URL$ - * $Id$ - *********************************************************************************** - * - * Copyright (c) 2003, 2004, 2005, 2006, 2007, 2008 The Sakai Foundation - * - * Licensed under the Educational Community 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.opensource.org/licenses/ECL-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 org.sakaiproject.chat2.model.impl; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.sakaiproject.chat2.model.ChatMessage; -import org.sakaiproject.chat2.model.ChatManager; -import org.sakaiproject.time.api.Time; -import org.sakaiproject.time.cover.TimeService; -import org.sakaiproject.chat2.model.ChatChannel; -import org.sakaiproject.component.cover.ComponentManager; -import org.sakaiproject.user.api.ContextualUserDisplayService; -import org.sakaiproject.user.api.User; -import org.sakaiproject.user.cover.UserDirectoryService; -import org.sakaiproject.user.api.UserNotDefinedException; -import org.sakaiproject.util.BaseDelivery; -import org.sakaiproject.util.StringUtil; -import org.sakaiproject.util.Web; - -/** - *

- * ChatDelivery is a Delivery that causes a chat message to be appended to a table of chat messages in the HTML element identified by the address and elementID. - *

- */ -public class ChatRestDelivery extends BaseDelivery -{ - /** Our logger. */ - private static Logger logger = LoggerFactory.getLogger(ChatRestDelivery.class); - - /** The message. it could be a string id or the actual message. */ - protected Object m_message = null; - - protected ChatManager chatManager = null; - - protected boolean m_beepOnDelivery = false; - - private ContextualUserDisplayService contextualUserDisplayService; - - /** - * Construct. - * - * @param address - * The address. - * @param elementId - * The elementId. - */ - public ChatRestDelivery(String address, String elementId, Object message, boolean beepOnDelivery, ChatManager chatManager) - { - super(address, elementId); - m_message = message; - m_beepOnDelivery = beepOnDelivery; - this.chatManager = chatManager; - } // ChatDelivery - - public ChatMessage getMessage() { - if(m_message instanceof String) { - m_message = chatManager.getMessage((String)m_message); - } - if(m_message instanceof ChatMessage) - return (ChatMessage)m_message; - return null; - } - - public void setMessage(Object message) { - this.m_message = message; - } - - /** - * Compose a javascript message for delivery to the browser client window. - * This function happens in the client connection thread instead of in the event notification thread - * - * @return The javascript message to send to the browser client window. - */ - public String compose() - { - ChatMessage message = null; - - if(m_message instanceof ChatMessage) { - message = (ChatMessage)m_message; - } else if(m_message instanceof String) { - message = chatManager.getMessage((String)m_message); - } else { - return ""; - } - if (logger.isDebugEnabled()) logger.debug("compose() element: " + m_elementId + ", message: " + message.getId()); - - // generate a string of JavaScript commands to update the message log - - User sender = null; - try { - sender = UserDirectoryService.getUser(message.getOwner()); - } catch(UserNotDefinedException e) { - logger.error(e.getMessage()); - } - User myself = UserDirectoryService.getCurrentUser(); - - ChatChannel channel = message.getChatChannel(); - - // We may not have a usage session - String retval = null; - - if (channel == null) - { - retval = "try { this.location.replace(addAuto(this.location));} catch (error) {alert(error);}"; - } - - // otherwise setup for a browser-side javascript DOM modification to insert the message - else - { - String msgbody = Web.escapeJsQuoted(Web.escapeHtmlFormattedText(message.getBody())); - msgbody = msgbody.replace('\n',' ').replace('\r',' '); - - Time messageTime = TimeService.newTime(message.getMessageDate().getTime()); - - StringBuilder retvalBuf = new StringBuilder(); - retvalBuf.append( "try { appendMessage('" ); - - String displayName = getUserDisplayName(sender, channel.getContext()); - retvalBuf.append( Web.escapeJsQuoted(Web.escapeHtml(displayName)) ); - - retvalBuf.append( "', '" ); - retvalBuf.append( sender.getId() ); - retvalBuf.append( "', '" ); - retvalBuf.append( String.valueOf(chatManager.getCanDelete(message)) ); - retvalBuf.append( "', '" ); - retvalBuf.append( messageTime.toStringLocalDate() ); - retvalBuf.append( "', '" ); - retvalBuf.append( messageTime.toStringLocalTimeZ() ); - retvalBuf.append( "', '" ); - retvalBuf.append( messageTime.toString() ); - retvalBuf.append( "', '" ); - retvalBuf.append( msgbody ); - retvalBuf.append( "','" ); - retvalBuf.append( message.getId() ); - retvalBuf.append( "'); } catch (error) {alert(error);} " ); - - retval = retvalBuf.toString(); - } - - if (m_beepOnDelivery && (sender != null) && sender.compareTo(myself) != 0) - { - retval += "beep = true;"; - } - - return retval; - - } // compose - - /** - * Display. - */ - public String toString() - { - return super.toString() + " : " + m_message; - - } // toString - - /** - * Are these the same? - * - * @return true if obj is the same Delivery as this one. - */ - public boolean equals(Object obj) - { - if (!super.equals(obj)) return false; - - ChatRestDelivery cob = (ChatRestDelivery) obj; - if (StringUtil.different(cob.getMessage().getId(), getMessage().getId() )) return false; - - return true; - } - - private String getUserDisplayName(User u, String context) { - contextualUserDisplayService = (ContextualUserDisplayService) ComponentManager.get("org.sakaiproject.user.api.ContextualUserDisplayService"); - - if (contextualUserDisplayService == null) { - return u.getDisplayName(); - } else { - String ret = contextualUserDisplayService.getUserDisplayName(u, "/site/" + context); - if (ret == null) - ret = u.getDisplayName(); - return ret; - - } - } -} diff --git a/chat/chat-impl/impl/src/java/org/sakaiproject/chat2/model/impl/ChatRestListener.java b/chat/chat-impl/impl/src/java/org/sakaiproject/chat2/model/impl/ChatRestListener.java deleted file mode 100644 index c3c8225b63d8..000000000000 --- a/chat/chat-impl/impl/src/java/org/sakaiproject/chat2/model/impl/ChatRestListener.java +++ /dev/null @@ -1,113 +0,0 @@ -package org.sakaiproject.chat2.model.impl; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.sakaiproject.chat2.model.ChatChannel; -import org.sakaiproject.chat2.model.ChatManager; -import org.sakaiproject.chat2.model.PresenceObserver; -import org.sakaiproject.chat2.model.RoomObserver; -import org.sakaiproject.courier.api.CourierService; -import org.sakaiproject.tool.cover.SessionManager; -import org.sakaiproject.util.DirectRefreshDelivery; - -public class ChatRestListener implements RoomObserver, PresenceObserver { - - private static final String IFRAME_ROOM_USERS = "Presence"; - - /** Our logger. */ - private static Logger LOG = LoggerFactory.getLogger(ChatRestListener.class); - - /** The work-horse of chat */ - private ChatManager chatManager; - - /** The id of the session. needed for adding messages to the courier because that runs in the notification thread */ - private String sessionId = ""; - - /** Channel for this listener */ - private ChatChannel channel = null; - - /** CourierService. */ - protected CourierService m_courierService = null; - - public ChatRestListener(ChatManager chatManager, CourierService courier, String sessionId, ChatChannel channel) { - this.chatManager = chatManager; - this.m_courierService = courier; - this.sessionId = sessionId; - this.channel = channel; - } - - public void receivedMessage(String roomId, Object message) { - - if (channel != null) { - if (!roomId.equals(channel.getId())) { - LOG.error("Incorrect channelId: room = " + roomId + " channelId = " + channel.getId()); - return; - } - - String address = sessionId + roomId; - - if (SessionManager.getSession(sessionId) == null) { - LOG.debug("received msg expired session " + sessionId + " " + channel); - m_courierService.clear(address); - } else { - m_courierService.deliver(new ChatRestDelivery(address, "Monitor", message, false, chatManager)); - } - } - - } - - public void roomDeleted(String roomId) { - - if (!roomId.equals(channel.getId())) { - LOG.error("Incorrect channelId: room = " + roomId + " channelId = " + channel.getId()); - return; - } - - resetCurrentChannel(); - m_courierService.clear(sessionId+roomId); - } - - public void userJoined(String location, String user) { - m_courierService.deliver(new DirectRefreshDelivery(sessionId+location, IFRAME_ROOM_USERS)); - } - - public void userLeft(String location, String user) { - if (channel != null && SessionManager.getSession(sessionId) == null) { - if (!location.equals(channel.getId())) { - LOG.error("Incorrect channelId: room = " + location + " channelId = " + channel.getId()); - return; - } - - resetCurrentChannel(); - m_courierService.clear(sessionId+location); - } - else - m_courierService.deliver(new DirectRefreshDelivery(sessionId+location, IFRAME_ROOM_USERS)); - - } - - // this removes the current channel but doesn't add a new one. sort of - // half of setCurrentChannel. - protected void resetCurrentChannel() { - /* - String channelId = oldChannel.getChatChannel().getId(); - String address = sessionId+channelId; - PresenceObserverHelper observer = presenceChannelObservers.get(channelId); - if (observer != null) { - observer.endObservation(); - observer.removePresence(); - getChatManager().removeRoomListener(this, channelId); - } - - m_courierService.clear(address); - presenceChannelObservers.remove(channelId); - channels.remove(channelId); - tools.remove(address); - currentChannel = null; -*/ - // System.out.println("resetcurrent channel " + presenceChannelObservers.size() + " " + channels.size() + " " + tools.size() ); - - } - - -} diff --git a/chat/chat-impl/impl/src/webapp/WEB-INF/components.xml b/chat/chat-impl/impl/src/webapp/WEB-INF/components.xml index 228856530b86..8e31c52e2984 100644 --- a/chat/chat-impl/impl/src/webapp/WEB-INF/components.xml +++ b/chat/chat-impl/impl/src/webapp/WEB-INF/components.xml @@ -21,8 +21,18 @@ init-method="init" destroy-method="destroy"> - + + + + + + + + + + + @@ -82,6 +92,9 @@ + + + diff --git a/chat/chat-tool/tool/pom.xml b/chat/chat-tool/tool/pom.xml index 259428b2812b..504a012d5980 100644 --- a/chat/chat-tool/tool/pom.xml +++ b/chat/chat-tool/tool/pom.xml @@ -37,22 +37,6 @@ org.sakaiproject sakai-chat-api
- - org.sakaiproject.courier - sakai-courier-api - - - org.sakaiproject.presence - sakai-presence-api - - - org.sakaiproject.presence - sakai-presence-util - - - org.sakaiproject.courier - sakai-courier-util - org.sakaiproject.site sakai-mergedlist-util diff --git a/chat/chat-tool/tool/src/bundle/chat.properties b/chat/chat-tool/tool/src/bundle/chat.properties index 84c6e3f22839..de3eea7ae83d 100644 --- a/chat/chat-tool/tool/src/bundle/chat.properties +++ b/chat/chat-tool/tool/src/bundle/chat.properties @@ -157,6 +157,9 @@ add_channel_title=Add Room message_display=Message Display messages_shown_total=Showing {0} messages out of {1} +messages_unreaded=There are {0} unread messages +new_messages=New messages +unreaded=Unread messages perm_error=Insufficient Permissions for {0} diff --git a/chat/chat-tool/tool/src/java/org/sakaiproject/chat2/tool/AlertDelivery.java b/chat/chat-tool/tool/src/java/org/sakaiproject/chat2/tool/AlertDelivery.java deleted file mode 100644 index 85c747d839ce..000000000000 --- a/chat/chat-tool/tool/src/java/org/sakaiproject/chat2/tool/AlertDelivery.java +++ /dev/null @@ -1,111 +0,0 @@ -/********************************************************************************** - * $URL: https://source.sakaiproject.org/svn/chat/trunk/chat-tool/tool/src/java/org/sakaiproject/chat/tool/ChatDelivery.java $ - * $Id: ChatDelivery.java 14062 2006-08-27 03:44:18Z csev@umich.edu $ - *********************************************************************************** - * - * Copyright (c) 2003, 2004, 2005, 2006, 2007, 2008 The Sakai Foundation - * - * Licensed under the Educational Community 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.opensource.org/licenses/ECL-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 org.sakaiproject.chat2.tool; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.sakaiproject.chat2.model.ChatMessage; -import org.sakaiproject.time.api.Time; -import org.sakaiproject.time.cover.TimeService; -//import org.sakaiproject.chat.cover.ChatService; -//import org.sakaiproject.entity.api.Reference; -//import org.sakaiproject.entity.cover.EntityManager; -import org.sakaiproject.event.api.UsageSession; -import org.sakaiproject.event.cover.UsageSessionService; -//import org.sakaiproject.exception.IdUnusedException; -//import org.sakaiproject.exception.PermissionException; -import org.sakaiproject.chat2.model.ChatChannel; -import org.sakaiproject.user.api.User; -import org.sakaiproject.user.cover.UserDirectoryService; -import org.sakaiproject.user.api.UserNotDefinedException; -import org.sakaiproject.util.BaseDelivery; -import org.sakaiproject.util.StringUtil; -import org.sakaiproject.util.Web; - -/** - *

- * ChatDelivery is a Delivery that causes a chat message to be appended to a table of chat messages in the HTML element identified by the address and elementID. - *

- */ -public class AlertDelivery extends BaseDelivery -{ - - /** - * Construct. - * - * @param address - * The address. - * @param elementId - * The elementId. - */ - public AlertDelivery(String address) - { - super(address, null); - - } // ChatDelivery - - /** - * Construct. - * - * @param address - * The address. - * @param elementId - * The elementId. - */ - public AlertDelivery(String address, String id) - { - super(address, id); - - } // ChatDelivery - - /** - * Compose a javascript message for delivery to the browser client window. - * - * @return The javascript message to send to the browser client window. - */ - public String compose() - { - if(getElement() == null) - return "alert(\"AlertDelivery\");"; - return "alert(" + getElement() + ");"; - - } // compose - - /** - * Display. - */ - public String toString() - { - return super.toString() + " : alert delivery"; - - } // toString - - /** - * Are these the same? - * - * @return true if obj is the same Delivery as this one. - */ - public boolean equals(Object obj) - { - return super.equals(obj); - } -} diff --git a/chat/chat-tool/tool/src/java/org/sakaiproject/chat2/tool/ChatDelivery.java b/chat/chat-tool/tool/src/java/org/sakaiproject/chat2/tool/ChatDelivery.java deleted file mode 100644 index 0346bdb93ff6..000000000000 --- a/chat/chat-tool/tool/src/java/org/sakaiproject/chat2/tool/ChatDelivery.java +++ /dev/null @@ -1,242 +0,0 @@ -/********************************************************************************** - * $URL: https://source.sakaiproject.org/svn/chat/trunk/chat-tool/tool/src/java/org/sakaiproject/chat/tool/ChatDelivery.java $ - * $Id: ChatDelivery.java 14062 2006-08-27 03:44:18Z csev@umich.edu $ - *********************************************************************************** - * - * Copyright (c) 2003, 2004, 2005, 2006, 2007, 2008 The Sakai Foundation - * - * Licensed under the Educational Community 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.opensource.org/licenses/ECL-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 org.sakaiproject.chat2.tool; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.sakaiproject.chat2.model.ChatMessage; -import org.sakaiproject.chat2.model.ChatManager; -import org.sakaiproject.time.api.Time; -import org.sakaiproject.time.cover.TimeService; -import org.sakaiproject.chat2.model.ChatChannel; -import org.sakaiproject.component.cover.ComponentManager; -import org.sakaiproject.component.cover.ServerConfigurationService; -import org.sakaiproject.courier.api.Expirable; -import org.sakaiproject.user.api.ContextualUserDisplayService; -import org.sakaiproject.user.api.User; -import org.sakaiproject.user.cover.UserDirectoryService; -import org.sakaiproject.user.api.UserNotDefinedException; -import org.sakaiproject.util.BaseDelivery; -import org.sakaiproject.util.StringUtil; -import org.sakaiproject.util.Web; - -/** - *

- * ChatDelivery is a Delivery that causes a chat message to be appended to a table of chat messages in the HTML element identified by the address and elementID. - *

- */ -public class ChatDelivery extends BaseDelivery implements Expirable -{ - /** Our logger. */ - private static Logger logger = LoggerFactory.getLogger(ChatDelivery.class); - - /** The message. it could be a string id or the actual message. */ - protected Object m_message = null; - - protected ChatManager chatManager = null; - - protected boolean m_beepOnDelivery = false; - - protected String placementId = ""; - - private ContextualUserDisplayService contextualUserDisplayService; - - private long created; - - private int ttl; - - /** - * Construct. - * - * @param address - * The address. - * @param elementId - * The elementId. - */ - public ChatDelivery(String address, String elementId, Object message, String placementId, boolean beepOnDelivery, ChatManager chatManager) - { - super(address, elementId); - m_message = message; - m_beepOnDelivery = beepOnDelivery; - this.chatManager = chatManager; - this.placementId = placementId; - this.created = System.currentTimeMillis(); - this.ttl = ServerConfigurationService.getInt("chat.delivery.ttl", 300); - } // ChatDelivery - - public ChatMessage getMessage() { - if(m_message instanceof String) { - m_message = chatManager.getMessage((String)m_message); - } - if(m_message instanceof ChatMessage) - return (ChatMessage)m_message; - return null; - } - - public void setMessage(Object message) { - this.m_message = message; - } - - /** - * Compose a javascript message for delivery to the browser client window. - * This function happens in the client connection thread instead of in the event notification thread - * - * @return The javascript message to send to the browser client window. - */ - public String compose() - { - ChatMessage message = null; - - if(m_message instanceof ChatMessage) { - message = (ChatMessage)m_message; - } else if(m_message instanceof String) { - message = chatManager.getMessage((String)m_message); - } else { - return ""; - } - logger.debug("compose() element: {}, message: {}", m_elementId, message.getId()); - - // generate a string of JavaScript commands to update the message log - - User sender = null; - try { - sender = UserDirectoryService.getUser(message.getOwner()); - } catch(UserNotDefinedException e) { - logger.error(e.getMessage()); - } - User myself = UserDirectoryService.getCurrentUser(); - - ChatChannel channel = message.getChatChannel(); - - // We may not have a usage session - String retval = null; - - if (channel == null) - { - retval = "try { this.location.replace(addAuto(this.location));} catch (error) {alert(error);}"; - } - - // otherwise setup for a browser-side javascript DOM modification to insert the message - else - { - String msgbody = Web.escapeJsQuoted(Web.escapeHtmlFormattedText(message.getBody())); - msgbody = msgbody.replace('\n',' ').replace('\r',' '); - - Time messageTime = TimeService.newTime(message.getMessageDate().getTime()); - - StringBuilder retvalBuf = new StringBuilder(); - retvalBuf.append( "try { appendMessage('" ); - - String displayName = getUserDisplayName(sender, channel.getContext()); - retvalBuf.append( Web.escapeJsQuoted(Web.escapeHtml(displayName)) ); - - retvalBuf.append( "', '" ); - retvalBuf.append( sender.getId() ); - retvalBuf.append( "', '" ); - retvalBuf.append( String.valueOf(chatManager.getCanDelete(message)) ); - retvalBuf.append( "', '" ); - retvalBuf.append( messageTime.toStringLocalDate() ); - retvalBuf.append( "', '" ); - retvalBuf.append( messageTime.toStringLocalTimeZ() ); - retvalBuf.append( "', '" ); - retvalBuf.append( messageTime.toString() ); - retvalBuf.append( "', '" ); - retvalBuf.append( msgbody ); - retvalBuf.append( "','" ); - retvalBuf.append( message.getId() ); - retvalBuf.append( "'); } catch (error) {alert(error);} " ); - - retval = retvalBuf.toString(); - } - - if (m_beepOnDelivery && (sender != null) && sender.compareTo(myself) != 0) - { - retval += "beep = true;"; - } - - return retval; - - } // compose - - /** - * Display. - */ - public String toString() - { - return super.toString() + " : " + m_message; - - } // toString - - public String getMessageId() { - if (m_message instanceof String) { - return (String)m_message; - } else if (m_message instanceof ChatMessage) { - return ((ChatMessage)m_message).getId(); - } else { - return ""; - } - } - - /** - * Are these the same? - * - * @return true if obj is the same Delivery as this one. - */ - public boolean equals(Object obj) - { - if (!super.equals(obj)) return false; - - ChatDelivery cob = (ChatDelivery) obj; - if (StringUtil.different(cob.getMessageId(), getMessageId())) return false; - - return true; - } - - private String getUserDisplayName(User u, String context) { - contextualUserDisplayService = (ContextualUserDisplayService) ComponentManager.get("org.sakaiproject.user.api.ContextualUserDisplayService"); - - if (contextualUserDisplayService == null) { - return u.getDisplayName(); - } else { - String ret = contextualUserDisplayService.getUserDisplayName(u, "/site/" + context); - if (ret == null) - ret = u.getDisplayName(); - return ret; - - } - } - - @Override - public long getCreated() { - return created; - } - - public void setCreated(long created) { - this.created = created; - } - - @Override - public int getTtl() { - return ttl; - } - -} diff --git a/chat/chat-tool/tool/src/java/org/sakaiproject/chat2/tool/ChatTool.java b/chat/chat-tool/tool/src/java/org/sakaiproject/chat2/tool/ChatTool.java index 581822013118..8dde01f2577e 100644 --- a/chat/chat-tool/tool/src/java/org/sakaiproject/chat2/tool/ChatTool.java +++ b/chat/chat-tool/tool/src/java/org/sakaiproject/chat2/tool/ChatTool.java @@ -23,12 +23,17 @@ import java.io.IOException; import java.text.MessageFormat; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; +import java.time.format.FormatStyle; import java.util.ArrayList; import java.util.Calendar; import java.util.Date; import java.util.HashMap; import java.util.Iterator; import java.util.List; +import java.util.Locale; import java.util.Map; import java.util.Map.Entry; import java.util.concurrent.ConcurrentHashMap; @@ -48,21 +53,16 @@ import org.sakaiproject.chat2.model.ChatMessage; import org.sakaiproject.chat2.model.ChatChannel; import org.sakaiproject.chat2.model.ChatManager; -import org.sakaiproject.chat2.model.RoomObserver; import org.sakaiproject.chat2.model.ChatFunctions; -import org.sakaiproject.chat2.model.PresenceObserver; -import org.sakaiproject.chat2.tool.ColorMapper; import org.sakaiproject.component.cover.ServerConfigurationService; import org.sakaiproject.component.cover.ComponentManager; import org.sakaiproject.event.api.EventTrackingService; -import org.sakaiproject.courier.api.CourierService; import org.sakaiproject.event.api.UsageSession; import org.sakaiproject.event.cover.UsageSessionService; import org.sakaiproject.exception.IdUnusedException; import org.sakaiproject.exception.PermissionException; import org.sakaiproject.site.api.Site; import org.sakaiproject.site.cover.SiteService; -import org.sakaiproject.time.cover.TimeService; import org.sakaiproject.tool.api.Placement; import org.sakaiproject.tool.api.Session; import org.sakaiproject.tool.api.Tool; @@ -73,73 +73,12 @@ import org.sakaiproject.user.api.UserNotDefinedException; import org.sakaiproject.user.cover.UserDirectoryService; import org.sakaiproject.util.DateFormatterUtil; -import org.sakaiproject.util.DirectRefreshDelivery; import org.sakaiproject.util.ResourceLoader; import org.sakaiproject.util.Validator; import org.sakaiproject.util.Web; -/** - * - * This class is used in two ways: - * 1) It is a JSF backing bean, with session scope. A new one is created - * every time the user enters the main chat tool. This includes a refresh. - * 2) It is an observer. It is put on a list of observers for the room - * by AddRoomListener. - * This double existence is messy, because JSF manages beans, but - * if JSF replaces or finishes the bean, there is no automatic way for - * it to get removed from the list of observers. - * Getting it off the list of observers is kind of tricky, because - * there's no direct way to know when the instance is no longer valid. - * Since a refresh generates a new instance, we have to get rid of the - * old one, or the list of observers keeps growing. The hash table - * "tools" is used to keep track of instances so we can kill old ones. - * This code was originally written when there was at most one chat - * room per site. For that it made sense for the backing bean to have - * session scope (because going to a new site goes through the main - * entry and generates a new instance). We really need to redo this with - * the bean at request scope and other data structures for things that - * live longer. But I found out about this issue a week before 2.6 was - * due. So I've adopted a hack. The bean is now in theory session scope - * but actually may be shared by more than one chat room in a site. - * It should never cross sites. That means there are now a couple of - * data structures indexed by room ID, to handle more than one room. - * The JSP's all set the current room ID when doing a submit. So - * that lets them share the single backing bean. - * The other way an instance can become invalid is if the session - * goes away. So in userLeft and a couple of other places we check - * we check whether the current session is - * still alive, and if not, remove the instance. - * This bookkeeping is done in SetCurrentChannel and - * ResetCurrentChannel, so they should be the only code to call - * AddRoomListener and RemoveRoomListener. - * For final cleanup we depend upon the fact that all observers will - * eventually get either UserLeft or ReceivedMessage, and the code will - * notice that the session no longer exists. At times UserLeft seems to be - * called when the session is still there, but when the next user logs out, - * it will get called again with no session. - * - * Chat works by the courier but not the same way as the old message delivery - * system. The courier sends messages into the JSF tool. When the user dials home - * it then picks up the messages. - * - * COOL TECHNOLOGY ALERT: - * TODO: The page that sends the messages is polled, the tool can wait on the - * connection until it recieves a message to send or until it times out. This - * would provide instant messaging as opposed to polling. - * - * When entering the tool the first main page calls the enterTool function which - * redirects the users based upon the tool's preferences: go to select a room or - * to a specific tool. NB: each time enterTool is called, we are dealing - * with a new instance. - * - * The PresenceObserverHelper is placed on the current room. This this tool is informed - * when a user enters and exits the room. - * - * - * @author andersjb - * - */ -public class ChatTool implements RoomObserver, PresenceObserver { + +public class ChatTool { /** Our logger. */ private static Logger logger = LoggerFactory.getLogger(ChatTool.class); @@ -159,9 +98,6 @@ public class ChatTool implements RoomObserver, PresenceObserver { private static final String PAGE_SYNOPTIC_OPTIONS = "synopticOptions"; private static final String PERMISSION_ERROR = "perm_error"; - private static final String PRESENCE_PREPEND = "chat_room_"; - - private static final String CHAT_CONTEXT_PRESENCE_PREFIX = "chat_site_"; private static final int MESSAGEOPTIONS_NULL = -99; private static final int MESSAGEOPTIONS_ALL_MESSAGES = -1; @@ -194,9 +130,6 @@ public class ChatTool implements RoomObserver, PresenceObserver { /** The tool manager */ private ToolManager toolManager; - /** Constructor discovered injected CourierService. */ - protected CourierService m_courierService = null; - /* All the private variables */ /** The current channel the user is in */ private DecoratedChatChannel currentChannel = null; @@ -218,36 +151,18 @@ public class ChatTool implements RoomObserver, PresenceObserver { /** display all messages (-1), past 3 days (0) */ private int messageOptions = MESSAGEOPTIONS_NULL; - /** The id of the session. needed for adding messages to the courier because that runs in the notification thread */ - private String sessionId = ""; - /** The id of the placement of this sakai tool. the jsf tool bean needs this for passing to the delivery */ private String placementId = ""; - /** Mapping the color of each message */ - private ColorMapper colorMapper = new ColorMapper(); - /** the worksite the tool is in */ private Site worksite = null; private String toolContext = null; - /* room id maps */ - private Map presenceChannelObservers = - new ConcurrentHashMap(); - private Map channels = - new ConcurrentHashMap(); - - /* address maps */ - private static Map tools = - new ConcurrentHashMap(); + // Used for fetching user's default language locale + ResourceLoader rl = new ResourceLoader(); protected void setupTool() { - // "inject" a CourierService - m_courierService = org.sakaiproject.courier.cover.CourierService.getInstance(); - - Session session = SessionManager.getCurrentSession(); - sessionId = session.getId(); Placement placement = getToolManager().getCurrentPlacement(); placementId = placement.getId(); @@ -297,184 +212,8 @@ public String getEnterTool() { return ""; } - /** - * this gets the users in a channel. It is typically called from roomUsers.jsp - * to display or refresh the list. Despite the name, if called from a JSP with - * channel as an argument, it uses that channel. Otherwise the current channel. - * - * @return List of String display names - */ - public List getUsersInCurrentChannel() - { - - ExternalContext context = FacesContext.getCurrentInstance().getExternalContext(); - HttpServletRequest req = (HttpServletRequest) context.getRequest(); - - // normally this comes from roomUsers.jsp, which has the channel as an argument - // no need to validate input as weird input simply results in a failure - String channelId = req.getParameter("channel"); - if (channelId == null) - channelId = getCurrentChatChannelId(); - - List userList = new ArrayList(); - - if (channelId == null) - return userList; - - PresenceObserverHelper observer = presenceChannelObservers.get(channelId); - - if(observer == null) - return userList; - - observer.updatePresence(); - - // get the current presence list (User objects) for this page - List users = observer.getPresentUsers(); - - if (users == null) - return userList; - - //System.out.println("userincurrent channel: " + getCurrentChatChannelId() + " users " + users); - - - // is the current user running under an assumed (SU) user id? - String asName = null; - String myUserId = null; - try { - - UsageSession usageSession = UsageSessionService.getSession(); - if (usageSession != null) { - - // this is the absolutely real end-user id, even if running as another user - myUserId = usageSession.getUserId(); - - // this is the user id the current user is running as - String sessionUserId = SessionManager.getCurrentSessionUserId(); - - // if different - if (!myUserId.equals(sessionUserId)) { - asName = UserDirectoryService.getUser(sessionUserId).getDisplayName(); - } - } - } catch (Throwable any) { - } - - for (Iterator i = users.iterator(); i.hasNext();) { - - User u = (User) i.next(); - String displayName = u.getDisplayName(); - - // adjust if this is the current user running as someone else - if ((asName != null) && (u.getId().equals(myUserId))) { - displayName += " (" + asName + ")"; - } - - userList.add(Web.escapeHtml(displayName)); - } - - //System.out.println("userincurrent channel return: " + userList); - - return userList; - } - - //******************************************************************** - // Interface Implementations - - /** - * {@inheritDoc} - * in the context of the event manager thread - */ - public void receivedMessage(String roomId, Object message) - { - - DecoratedChatChannel channel = channels.get(roomId); - // System.out.println("receivedmessager " + sessionId + " " + roomId + " " + channel + " " + SessionManager.getSession(sessionId)); - if (channel != null) { - - String address = sessionId + roomId; - if (SessionManager.getSession(sessionId) == null) { - // System.out.println("expire session " + sessionId + " " + currentChannel); - logger.debug("received msg expired session " + sessionId + " " + currentChannel); - resetCurrentChannel(channel); - m_courierService.clear(address); - } else { - m_courierService.deliver(new ChatDelivery(address, "Monitor", message, placementId, false, getChatManager())); - } - } - } - - /** - * {@inheritDoc} - */ - public void roomDeleted(String roomId) - { - DecoratedChatChannel channel = channels.get(roomId); - if (channel != null) { - resetCurrentChannel(channel); - m_courierService.clear(sessionId+roomId); - } - } - - public class MyAddDelivery extends DirectRefreshDelivery { - private String m_username; - public void setUsername(String username) { - m_username = username; - } - - public MyAddDelivery(String address, String elementId, String username) { - super(address, elementId); - m_username = username; - } - public String compose() { - return "addUser('" + m_username + "')"; - } - } - - public class MyDelDelivery extends DirectRefreshDelivery { - private String m_username; - - public MyDelDelivery(String address, String elementId, String username) { - super(address, elementId); - m_username = username; - } - public String compose() { - return "delUser('" + m_username + "')"; - } - } - - /** - * {@inheritDoc} - */ - public void userJoined(String location, String user) - { - MyAddDelivery delivery = new MyAddDelivery(sessionId+location, IFRAME_ROOM_USERS, user); - m_courierService.deliver(delivery); - } - - /** - * {@inheritDoc} - * In addition to refreshing the list, this code takes care of - * removing instances associated with processes that are now dead. - * Despite the user argument, the API doesn't tell us what user - * left. So the best we can do is check ourself, and rely on the - * fact that this will be called once for each user who has an - * observer in the room observer list, so the user who left - * should catch himself. - */ - - // new impl that counts the number of system in the location - public void userLeft(String location, String user) - { - - DecoratedChatChannel channel = channels.get(location); - // System.out.println("userLeft " + sessionId + " " + location + " " + channel + " " + SessionManager.getSession(sessionId)); - if (channel != null && SessionManager.getSession(sessionId) == null) { - // System.out.println("expire session " + sessionId + " " + currentChannel); - resetCurrentChannel(channel); - m_courierService.clear(sessionId+location); - } - else - m_courierService.deliver(new MyDelDelivery(sessionId+location, IFRAME_ROOM_USERS, user)); + public int getPollInterval() { + return getChatManager().getPollInterval(); } //******************************************************************** @@ -662,6 +401,14 @@ public String getPermissionsMessage() { return getMessageFromBundle("perm_description", new Object[]{ getToolManager().getCurrentTool().getTitle(), getWorksite().getTitle()}); } + + private String getFormattedDate(Date d){ + ZonedDateTime ldt = ZonedDateTime.ofInstant(d.toInstant(), ZoneId.of(chatManager.getUserTimeZone())); + Locale locale = rl.getLocale(); + DateTimeFormatter dtf = DateTimeFormatter.ofLocalizedDate(FormatStyle.MEDIUM).withLocale(locale); + + return ldt.format(dtf); + } /** * @return the translated message based on the current date settings for this channel @@ -671,16 +418,16 @@ public String getDatesMessage() { if (this.currentChannel != null) { if (this.currentChannel.getStartDate() != null && this.currentChannel.getEndDate() != null) { msg = getMessageFromBundle("custom_date_display", - new Object[] {TimeService.newTime(this.currentChannel.getStartDate().getTime()).toStringLocalDate(), - TimeService.newTime(this.currentChannel.getEndDate().getTime()).toStringLocalDate()} + new Object[] {getFormattedDate(this.currentChannel.getStartDate()), + getFormattedDate(this.currentChannel.getEndDate())} ); } else if (this.currentChannel.getStartDate() != null) { msg = getMessageFromBundle("custom_date_display_start", - new Object[] {TimeService.newTime(this.currentChannel.getStartDate().getTime()).toStringLocalDate(), ""} + new Object[] {getFormattedDate(this.currentChannel.getStartDate()), ""} ); } else if (this.currentChannel.getEndDate() != null) { msg = getMessageFromBundle("custom_date_display_end", - new Object[] {"", TimeService.newTime(this.currentChannel.getEndDate().getTime()).toStringLocalDate()} + new Object[] {"", getFormattedDate(this.currentChannel.getEndDate())} ); } } @@ -984,23 +731,6 @@ public String processActionDeleteMessageCancel() //******************************************************************** // Getters and Setters - - /** - * This allows us to change/give the courier address. - * We want the courier to respond to the chat room. - * @return String - */ - public String getCourierString() { - StringBuilder courierString = new StringBuilder("/courier/"); - courierString.append(getCurrentChatChannelId()); - courierString.append("/"); - courierString.append(CHAT_CONTEXT_PRESENCE_PREFIX); - courierString.append(getContext()); - courierString.append("?userId="); - courierString.append(SessionManager.getCurrentSessionUserId()); - - return courierString.toString(); - } /** * Check for add/edit/del perms on the channel @@ -1047,8 +777,7 @@ public DecoratedChatChannel getCurrentChannel() } /** - * Implements a change of the chat room. It removes presence from the prior room, - * adds observation of the new room, and then becomes present in the new room + * Implements a change of the chat room. * @param channel */ public void setCurrentChannel(DecoratedChatChannel channel) @@ -1062,57 +791,9 @@ public void setCurrentChannel(DecoratedChatChannel channel) channel.getChatChannel().getId())) { return; } - - // turn off observation for the old channel - String channelId = null; - if (channel != null) { - channelId = channel.getChatChannel().getId(); - String address = sessionId+channelId; - DecoratedChatChannel oldChannel = channels.get(channelId); - if (oldChannel != null) { - resetCurrentChannel(oldChannel); - } - ChatTool tool = tools.remove(address); - if (tool != null) { - tool.resetCurrentChannel(channel); - } - } this.currentChannel = channel; - - if (channel != null) { - // place a presence observer on this tool. - PresenceObserverHelper helper = new PresenceObserverHelper(this, channelId); - presenceChannelObservers.put(channelId, helper); - tools.put(sessionId+channelId, this); - // hmmmm.... should this all be under the synchronize? - getChatManager().addRoomListener(this, channelId); - channels.put(channelId, channel); - helper.updatePresence(); - } } - - // this removes the current channel but doesn't add a new one. sort of - // half of setCurrentChannel. - protected void resetCurrentChannel(DecoratedChatChannel oldChannel) { - String channelId = oldChannel.getChatChannel().getId(); - String address = sessionId+channelId; - PresenceObserverHelper observer = presenceChannelObservers.get(channelId); - if (observer != null) { - observer.endObservation(); - observer.removePresence(); - getChatManager().removeRoomListener(this, channelId); - } - - m_courierService.clear(address); - presenceChannelObservers.remove(channelId); - channels.remove(channelId); - tools.remove(address); - currentChannel = null; - - // System.out.println("resetcurrent channel " + presenceChannelObservers.size() + " " + channels.size() + " " + tools.size() ); - - } /** * @return the currentChannelEdit @@ -1138,7 +819,7 @@ public DecoratedChatMessage getCurrentMessage() { if(messageId != null) { ChatMessage message = getChatManager().getMessage(messageId); if(message==null) return null; - tmpCurrent = new DecoratedChatMessage(this, message); + tmpCurrent = new DecoratedChatMessage(this, message, chatManager); return tmpCurrent; } } @@ -1294,14 +975,6 @@ public ToolManager getToolManager() { public void setToolManager(ToolManager toolManager) { this.toolManager = toolManager; } - - public ColorMapper getColorMapper() { - return colorMapper; - } - - public void setColorMapper(ColorMapper colorMapper) { - this.colorMapper = colorMapper; - } public boolean getDisplayDate() { @@ -1514,7 +1187,7 @@ protected List getMessages(String context, Date limitDate, List decoratedMessages = new ArrayList(); for (ChatMessage message : messages) { - DecoratedChatMessage decoratedMessage = new DecoratedChatMessage(this, message); + DecoratedChatMessage decoratedMessage = new DecoratedChatMessage(this, message, chatManager); decoratedMessages.add(decoratedMessage); } return decoratedMessages; @@ -1601,6 +1274,10 @@ public String getMessagesShownTotalText() { return getMessageFromBundle("messages_shown_total", new Object[]{"*SHOWN*","*TOTAL*"}); } + public String getUnreadedMessagesText() { + return getMessageFromBundle("messages_unreaded", new Object[]{"*UNREADED*"}); + } + public String getViewingChatRoomText() { String title = null; DecoratedChatChannel dChannel = getCurrentChannel(); @@ -1703,10 +1380,6 @@ public String getToolContext() { public void setToolContext(String toolContext) { this.toolContext = toolContext; } - - public String getSessionId() { - return sessionId; - } public void validatePositiveNumber(FacesContext context, UIComponent component, Object value){ if (value != null) diff --git a/chat/chat-tool/tool/src/java/org/sakaiproject/chat2/tool/ColorMapper.java b/chat/chat-tool/tool/src/java/org/sakaiproject/chat2/tool/ColorMapper.java deleted file mode 100644 index f701db5f0545..000000000000 --- a/chat/chat-tool/tool/src/java/org/sakaiproject/chat2/tool/ColorMapper.java +++ /dev/null @@ -1,159 +0,0 @@ -/********************************************************************************** - * $URL: https://source.sakaiproject.org/svn/chat/trunk/chat-tool/tool/src/java/org/sakaiproject/chat/tool/ColorMapper.java $ - * $Id: ColorMapper.java 8206 2006-04-24 19:40:15Z ggolden@umich.edu $ - *********************************************************************************** - * - * Copyright (c) 2003, 2004, 2005, 2006, 2007, 2008 The Sakai Foundation - * - * Licensed under the Educational Community 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.opensource.org/licenses/ECL-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 org.sakaiproject.chat2.tool; - - -import java.lang.reflect.Array; -import java.util.List; -import java.util.ArrayList; -import java.util.Hashtable; -import java.util.Iterator; -import java.util.Map; - -/** -*

ColorMapper is a wrapper for a Hashtable that maps user names (or any set of Strings) to colors.

-*

The colors are standard names for HTML colors.

-*/ -public class ColorMapper -{ - // The index of the next color in the COLORS array that will be assigned to a name - protected int m_next = 0; - - // A mapping of names to colors - protected Map m_map; - - // An array of Strings representing standard HTML colors. - protected static final String[] COLORS = - { "red", "blue", "green", "orange", "firebrick", "teal", "goldenrod", - "darkgreen", "darkviolet", "lightslategray", "peru", "deeppink", "dodgerblue", - "limegreen", "rosybrown", "cornflowerblue", "crimson", "turquoise", "darkorange", - "blueviolet", "royalblue", "brown", "magenta", "olive", "saddlebrown", "purple", - "coral", "mediumslateblue", "sienna", "mediumturquoise", "hotpink", "lawngreen", - "mediumvioletred", "slateblue", "indianred", "slategray", "indigo", "darkcyan", - "springgreen", "darkgoldenrod", "steelblue", "darkgray", "orchid", "darksalmon", - "lime", "gold", "darkturquoise", "navy", "orangered", "darkkhaki", "darkmagenta", - "darkolivegreen", "tomato", "aqua", "darkred", "olivedrab" - }; - - // the size of the COLORS array - protected static final int NumColors = Array.getLength(COLORS); - - /** - * Construct the ColorMapper. - */ - public ColorMapper() - { - m_map = new Hashtable(); - - } // ColorMapper - - /** - * get the color associated with a string. if name not already associated with a color, - * make a new association and determine the next color that will be used. the same string will - * always have the same color - */ - public String getColor(String name) - { - String color; - if(m_map.containsKey(name)) - { - color = (String) m_map.get(name); - } - else - { - color = COLORS[m_next++]; - m_map.put(name, color); - if(m_next >= NumColors) - { - m_next = 0; - } - } - - return color; - - } // getColor - - /** - * Returns the mapping of names to colors. - */ - public Map getMapping() - { - return m_map; - - } // getMapping - - public class KeyValue { - String k,v; - public KeyValue(String k, String v){this.k = k; this.v = v;} - public String getKey() {return k;} - public String getValue() {return v;} - } - - /** - * Returns the mapping of names to colors. - */ - public List getMappingList() - { - List mapList = new ArrayList(); - - for(Iterator i = m_map.keySet().iterator(); i.hasNext(); ) { - String key = (String)i.next(); - String value = (String)m_map.get(key); - - mapList.add(new KeyValue(key,value)); - } - - return mapList; - - } // getMapping - - /** - * Returns the index of the next color in the COLORS array that will be assigned to a name. - */ - public int getNext() - { - return m_next; - - } // getNext - - /** - * Returns the entire array of color names. - */ - public String[] getColors() - { - return COLORS; - - } // getColors - - /** - * Returns the size of the array of color names. - */ - public int getNum_colors() - { - return NumColors; - - } // getNum_colors - -} // ColorMapper - - - diff --git a/chat/chat-tool/tool/src/java/org/sakaiproject/chat2/tool/DecoratedChatMessage.java b/chat/chat-tool/tool/src/java/org/sakaiproject/chat2/tool/DecoratedChatMessage.java index e7f76cd9d24d..884a421bc7d2 100644 --- a/chat/chat-tool/tool/src/java/org/sakaiproject/chat2/tool/DecoratedChatMessage.java +++ b/chat/chat-tool/tool/src/java/org/sakaiproject/chat2/tool/DecoratedChatMessage.java @@ -21,10 +21,15 @@ package org.sakaiproject.chat2.tool; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; +import java.time.format.FormatStyle; + +import org.sakaiproject.chat2.model.ChatManager; import org.sakaiproject.chat2.model.ChatMessage; -import org.sakaiproject.time.api.Time; -import org.sakaiproject.time.cover.TimeService; import org.sakaiproject.util.FormattedText; +import org.sakaiproject.util.ResourceLoader; public class DecoratedChatMessage { @@ -32,23 +37,22 @@ public class DecoratedChatMessage { private ChatTool chatTool; - private Time messageTime; + private ChatManager chatManager; + + private ZonedDateTime ldt; + ResourceLoader rl = new ResourceLoader(); - public DecoratedChatMessage(ChatTool chatTool, ChatMessage chatMessage) + public DecoratedChatMessage(ChatTool chatTool, ChatMessage chatMessage, ChatManager chatManager) { this.chatTool = chatTool; this.chatMessage = chatMessage; + this.chatManager = chatManager; if (chatMessage != null && chatMessage.getMessageDate() != null) { - messageTime = TimeService.newTime(chatMessage.getMessageDate().getTime()); + ldt = ZonedDateTime.ofInstant(chatMessage.getMessageDate().toInstant(), ZoneId.of(chatManager.getUserTimeZone())); } } - public String getColor() - { - return chatTool.getColorMapper().getColor(chatMessage.getOwner()); - } - public ChatMessage getChatMessage() { return chatMessage; @@ -56,22 +60,22 @@ public ChatMessage getChatMessage() public String getDateTime() { - return messageTime.toStringLocalFullZ(); + return ldt.format(DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM, FormatStyle.LONG).withLocale(rl.getLocale())); } public String getDate() { - return messageTime.toStringLocalDate(); + return ldt.format(DateTimeFormatter.ofLocalizedDate(FormatStyle.MEDIUM).withLocale(rl.getLocale())); } public String getTime() { - return messageTime.toStringLocalTimeZ(); + return ldt.format(DateTimeFormatter.ofLocalizedTime(FormatStyle.LONG).withLocale(rl.getLocale())); } public String getId() { - return messageTime.toString(); + return ldt.format(DateTimeFormatter.ofPattern("yyyyMMddHHmmssSSS", rl.getLocale())); } /** diff --git a/chat/chat-tool/tool/src/java/org/sakaiproject/chat2/tool/PresenceObserverHelper.java b/chat/chat-tool/tool/src/java/org/sakaiproject/chat2/tool/PresenceObserverHelper.java deleted file mode 100644 index 199c8c5ade91..000000000000 --- a/chat/chat-tool/tool/src/java/org/sakaiproject/chat2/tool/PresenceObserverHelper.java +++ /dev/null @@ -1,177 +0,0 @@ -/********************************************************************************** - * $URL: https://source.sakaiproject.org/svn/presence/trunk/presence-util/util/src/java/org/sakaiproject/util/PresenceObservingCourier.java $ - * $Id: PresenceObservingCourier.java 8204 2006-04-24 19:35:57Z ggolden@umich.edu $ - *********************************************************************************** - * - * Copyright (c) 2003, 2004, 2005, 2006, 2007, 2008 The Sakai Foundation - * - * Licensed under the Educational Community 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.opensource.org/licenses/ECL-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 org.sakaiproject.chat2.tool; - -import org.sakaiproject.chat2.model.PresenceObserver; - -import java.util.List; -import java.util.Observable; -import java.util.Observer; - -import org.sakaiproject.event.api.Event; -import org.sakaiproject.event.api.EventTrackingService; -import org.sakaiproject.presence.cover.PresenceService; -import org.sakaiproject.user.cover.UserDirectoryService; -import org.sakaiproject.event.cover.UsageSessionService; -import org.sakaiproject.user.api.User; -import org.sakaiproject.event.api.UsageSession; - -/** - *

- * PresenceObservingCourier is an EventObservingCourier which watches for only presence service events at a particular location, and delivers a direct refresh delivery. - *

- */ -public class PresenceObserverHelper implements Observer -{ - /** Constructor discovered injected EventTrackingService. */ - protected EventTrackingService m_eventTrackingService = null; - - private String m_resourcePattern; - private String location; - - private PresenceObserver presenceObserver; - - /** - * This variant, watches presense changes at - * the specified location, and sends the notifications to - * that same location. The elementID is null so the main window is refreshed when the notification - * is received. - * - * @param location - * The location under observation *and* the location for the delivery of the events. - */ - public PresenceObserverHelper(PresenceObserver presenceObserver, String location) - { - this.presenceObserver = presenceObserver; - this.location = location; - m_resourcePattern = PresenceService.presenceReference(location); - - // "inject" a eventTrackingService - m_eventTrackingService = org.sakaiproject.event.cover.EventTrackingService.getInstance(); - - // register to listen to events - m_eventTrackingService.addObserver(this); - // %%% add the pattern to have it filtered there? - } - - protected void finalize() - { - // stop observing the presence location - m_eventTrackingService.deleteObserver(this); - } - - public void endObservation() - { - // stop observing the presence location - m_eventTrackingService.deleteObserver(this); - } - - public void updatePresence() - { - PresenceService.setPresence(location); - } - - public void removePresence() - { - PresenceService.removePresence(location); - } - - public List getPresentUsers() - { - return PresenceService.getPresentUsers(location); - } - - public String getLocation() - { - return location; - } - - /** - * Check to see if we want to process or ignore this update. - * - * @param arg - * The arg from the update. - * @return true to continue, false to quit. - */ - public boolean check(Object arg) - { - // arg is Event - if (!(arg instanceof Event)) return false; - Event event = (Event) arg; - String key = event.getResource(); - - // reject non presence events - String function = event.getEvent(); - if (!(function.equals(PresenceService.EVENT_PRESENCE) || function.equals(PresenceService.EVENT_ABSENCE))) return false; - - // look for matches to the pattern - if (m_resourcePattern != null) - { - if (!key.equals(m_resourcePattern)) return false; - } - - return true; - } - - /** - * This method is called whenever the observed object is changed. An application calls an Observable object's notifyObservers method to have all the object's observers notified of the change. default implementation is to - * cause the courier service to deliver to the interface controlled by my controller. Extensions can override. - * - * @param o - * the observable object. - * @param arg - * an argument passed to the notifyObservers method. - */ - public void update(Observable o, Object arg) - { - if (!check(arg)) return; - - Event event = (Event) arg; - String username = ""; - - String userId = event.getUserId(); - if (userId == null) { - String sessionId = event.getSessionId(); - if (sessionId != null) { - UsageSession session = UsageSessionService.getSession(sessionId); - if (session != null) { - userId = session.getUserId(); - } - } - } - - User user = null; - if (userId != null) - try { - user = UserDirectoryService.getUser(userId); - } catch (Exception e) {} - if (user != null) - username = user.getDisplayName(); - - if(event.getEvent().equals(PresenceService.EVENT_PRESENCE)) - presenceObserver.userJoined(location, username); - else - presenceObserver.userLeft(location, username); - - } -} - diff --git a/chat/chat-tool/tool/src/webapp/WEB-INF/faces-config.xml b/chat/chat-tool/tool/src/webapp/WEB-INF/faces-config.xml index df902a87b48d..781de26eb537 100644 --- a/chat/chat-tool/tool/src/webapp/WEB-INF/faces-config.xml +++ b/chat/chat-tool/tool/src/webapp/WEB-INF/faces-config.xml @@ -78,11 +78,6 @@ /jsp/room.jsp - - roomActions - /jsp/roomActions.jsp - - editRoom /jsp/editRoom.jsp @@ -93,11 +88,6 @@ /jsp/listRooms.jsp - - deleteMessageConfirm - /jsp/deleteMessageConfirm.jsp - - deleteRoomConfirm /jsp/deleteRoomConfirm.jsp diff --git a/chat/chat-tool/tool/src/webapp/js/chatscript.js b/chat/chat-tool/tool/src/webapp/js/chatscript.js index 1852dc9a0f1d..c0fb4ac5fd66 100644 --- a/chat/chat-tool/tool/src/webapp/js/chatscript.js +++ b/chat/chat-tool/tool/src/webapp/js/chatscript.js @@ -3,7 +3,7 @@ * $Id: $ *********************************************************************************** * - * Copyright (c) 2003, 2004, 2005, 2006, 2007, 2008, 2009 The Sakai Foundation + * Copyright (c) 2017 The Sakai Foundation * * Licensed under the Educational Community License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,289 +18,302 @@ * limitations under the License. * **********************************************************************************/ +var chatscript = { + url_submit : "/direct/chat-message/", + keycode_enter : 13, + pollInterval : 5000, + currentChatChannelId : null, + timeoutVar : null, + + init : function(){ + var me = this; + + me.updateShownText(); + me.scrollChat(); + me.updateChatData(); + + var textarea = $("#topForm\\:controlPanel\\:message"); + var submitButton = $("#topForm\\:controlPanel\\:submit"); + var resetButton = $("#topForm\\:controlPanel\\:reset"); + submitButton.bind('click', function() { + var messageBody = textarea.val(); + submitButton.prop("disabled", true); + var params = { + "chatChannelId": me.currentChatChannelId, + "body": messageBody + } + // If message body or currentChatChannelId are empty + if (!me.currentChatChannelId || !messageBody || !messageBody.replace(/\n/g, "").replace(/ /g, "").length) { + textarea.val("").focus(); + submitButton.prop("disabled", false); + return false; + } + me.sendMessage(params, textarea, submitButton); + }); + textarea.keydown(function(e) { + var keycode = e.keyCode; + if (keycode == me.keycode_enter && !submitButton.prop("disabled")) { + submitButton.trigger("click"); + return false; + } + }); + resetButton.click(function() { + textarea.val("").focus(); + }); -// add a message to the chat list from chef_chat-List.vm -function appendMessage(uname, uid, removeable, pdate, ptime, pid, msg, msgId) -{ - var undefined; - var position = 100000, docheight = 0, frameheight = 300; - var transcript = document.getElementById("topForm:chatList"); - - // compose the time/date according to user preferences for this session - var msgTime = ""; - if(window.display_date && window.display_time) - { - msgTime = " " + pdate + " " + ptime + " " ; - } - else if (window.display_date) - { - msgTime = " " + pdate + " " ; - } - else if(window.display_time) - { - msgTime = " " + ptime + " " ; - } - else if(window.display_id) - { - msgTime = " (" + pid + ") " ; - } + $(".chatList").on('click', '.chatRemove', function() { + var messageItem = $(this).parent().parent(); + var messageId = messageItem.attr("data-message-id"); + var ownerDisplayName = messageItem.find(".chatName").text(); + var date = messageItem.find(".chatDate").text(); + var messageBody = messageItem.find(".chatText").html(); + me.showRemoveModal(messageId, ownerDisplayName, date, messageBody); + }); + $("#deleteButton").click(function() { + var messageId = $(this).attr("data-message-id"); + me.deleteMessage(messageId); + }); - var newDiv = document.createElement('li'); - newDiv.setAttribute("data-message-id", msgId); - newDiv.setAttribute("data-owner-id", uid); - var color = ColorMap[uid]; - if(color == null) - { - color = Colors[nextColor++]; - ColorMap[uid] = color; - if(nextColor >= numColors) - { - nextColor = 0; + $("#chatListWrapper").scroll(function() { + if (($(".chatListWrapper")[0].scrollHeight - $(".chatListWrapper").scrollTop()) <= $(".chatListWrapper").height()) { + setTimeout(function() { + $(".divisorNewMessages[id!=divisorNewMessages]").fadeOut(300, function() { + $(".divisorNewMessages[id!=divisorNewMessages]").remove(); + }); + }, 3000); + $(".scrollBottom").fadeOut(300, function() { + unreadedMessages = 0; + me.updateUnreadedMessages(); + }); + } + }); + $(".scrollBottom").click(function() { + me.scrollChat(); + setTimeout(function() { + $(".divisorNewMessages[id!=divisorNewMessages]").fadeOut(300, function() { + $(".divisorNewMessages[id!=divisorNewMessages]").remove(); + }); + }, 3000); + $(".scrollBottom").fadeOut(300, function() { + unreadedMessages = 0; + me.updateUnreadedMessages(); + }); + }); + }, + sendMessage : function(params, textarea, submitButton) { + var me = this; + var errorSubmit = $("#errorSubmit"); + $.ajax({ + url: me.url_submit + 'new', + data: params, + type: "POST", + beforeSend: function() { + errorSubmit.slideUp('fast'); + }, + error: function(xhr, ajaxOptions, thrownError) { + errorSubmit.slideDown('fast'); + textarea.focus(); + submitButton.prop("disabled", false); + }, + success: function(data) { + me.scrollChat(); + me.updateChatData(); + textarea.val("").focus(); + submitButton.prop("disabled", false); + } + }); + }, + deleteMessage : function(messageId) { + var me = this; + var removeModal = $("#removemodal"); + removeModal.modal("hide"); + $.ajax({ + url: me.url_submit + messageId, + type: "DELETE", + success: function(data) { + removeModal.modal("hide"); + me.updateChatData(); + } + }); + }, + updateChatData : function () { + var me = this; + this.doUpdateChatData(); + if(this.timeoutVar != null) { + clearTimeout(this.timeoutVar); } - } - - var deleteHtml = ""; - if (removeable == "true") - { - newComponentId = $(transcript).children("li").size(); - var builtId = "topForm:chatList:" + newComponentId + ":deleteMessage"; - var tmpdeleteUrl = deleteUrl + msgId; - deleteHtml = - " " + - ""; - } - - var isOnline = $("#presence").find("li:contains('" + uname + "')"); - var lastMessageOwnerId = $("#topForm\\:chatList li:last-of-type").attr("data-owner-id"); - if (lastMessageOwnerId == uid) { - newDiv.className = "prev"; - } - - var innerHTML = '' + - '' + - ' ' + - '' + - '' + uname + '' + - '' + msgTime + '' + - '' + - '' + - '' + msgTime + '' + - '' + msg + '' + deleteHtml + - ''; - newDiv.innerHTML = innerHTML; - - transcript.appendChild(newDiv); - - // adjust scroll - var objDiv = document.getElementById("Monitor"); - objDiv.scrollTop = objDiv.scrollHeight; - - // update the messages count - chat2_totalMessages++; - chat2_shownMessages++; - updateShownText(); - - scrollChat(); -} - -function updateShownText() { - var countText = chat2_messageCountTemplate + ''; - countText = countText.replace('*SHOWN*', chat2_shownMessages); - countText = countText.replace('*TOTAL*', chat2_totalMessages); - $("#chat2_messages_shown_total").html(countText); -} - -// can't just pass a list of
  • 's, because $(string) will only parse a single object -function sortChildren(list) { - var children = list.children('li'); - - // sort uses last name if present, and is case-insensitive. - children = children.sort(function(a,b){ - var an = a.innerHTML.toLowerCase(); - var i = an.indexOf(' '); - if (i >= 0) - an = an.substring(i); - var bn = b.innerHTML.toLowerCase(); - var j = bn.indexOf(' '); - if (j >= 0) - bn = bn.substring(j); - - if(an > bn) { - return 1; - } - if(an < bn) { - return -1; - } - // equal, take full name first - if (i <= 0 && j >= 0) - return 1; - if (j <= 0 && i >= 0) - return -1; - // equal, now do it on original so uppercase comes first - an = a.innerHTML; - bn = b.innerHTML; - if(an > bn) { - return 1; - } - if(an < bn) { - return -1; - } - return 0; - }); - list.empty(); - list.append(children); - return list; -} - -function addUser(user) { - var existing = $("#presence").find("li:contains('" + user + "')"); - if (existing.size() == 0) { - $("#presence").append($('
  • ' + user + '
  • ')); - var newChildren = sortChildren($("#presence")).children(); - $("#presence").empty(); - $("#presence").append(newChildren); - $(".chatNameDate:contains(" + user + ")").parent().find(".chatUserOnline").addClass("is-online"); - } -} - -function delUser(user) { - $("#presence").find("li:contains('" + user + "')").remove(); - $(".chatNameDate:contains(" + user + ")").parent().find(".chatUserOnline").removeClass("is-online"); -} - -function updateUsers() { - var url = "roomUsers?channel=" + currentChatChannelId; - $.ajax({ - url: url, - type: "GET"}) - .done(function(data) { - var newChildren = sortChildren($('
      ' + data + '
    ')).children(); - $("#presence").empty(); - $("#presence").append(newChildren); - for (var i=0; i $(".chatListWrapper").height()) { + scrolledToBottom = false; } - }); -} -function scrollChat() { - // Scroll chat to last message - var scrollableChat = $("#chatListWrapper"); - scrollableChat.scrollTop(scrollableChat.prop("scrollHeight")); -} + if (messages.length > 0 && !scrolledToBottom) { + if ($(".divisorNewMessages").length < 2) { + var divisorNewMessages = $("#divisorNewMessages").clone(); + divisorNewMessages.removeAttr("id"); + divisorNewMessages.removeClass("hide"); + $("#topForm\\:chatList").append(divisorNewMessages); + } + } -//Library to ajaxify the Chatroom message submit action - $(document).ready(function() { - updateShownText(); - updateUsers(); - scrollChat(); + for (var i=0; i= 0) { - frameless = true; - } + var messageBody = messages[i].body; + var removeable = messages[i].removeable; + var exists = $("#topForm\\:chatList li[data-message-id=" + messageId + "]").length; + if (!exists) { + var messageItem = $("#chatListItem").clone(); + messageItem.removeClass("hide"); + messageItem.removeAttr("id"); + messageItem.attr("data-message-id", messageId); + messageItem.attr("data-owner-id", ownerId); + messageItem.find(".chatUserAvatar").css("background-image", "url(/direct/profile/" + ownerId + "/image)"); + messageItem.find(".chatMessage").attr("data-message-id", messageId); + messageItem.find(".chatName").attr("id", ownerId); + messageItem.find(".chatName").text(ownerDisplayName); + messageItem.find(".chatDate").text(dateStr); + messageItem.find(".chatText").html(messageBody); + if (removeable) { + messageItem.find(".chatRemove").removeClass("hide"); + } + if (lastMessageOwnerId == ownerId) { + messageItem.addClass("nestedMessage"); + messageItem.find(".chatMessageDate").text(dateStr); + } + $("#topForm\\:chatList").append(messageItem); + someMessageAdded = true; + chat2_totalMessages++; + chat2_shownMessages++; + this.updateShownText(); + if (scrolledToBottom) { + this.scrollChat(); + setTimeout(function() { + $(".divisorNewMessages[id!=divisorNewMessages]").fadeOut(300, function() { + $(".divisorNewMessages[id!=divisorNewMessages]").remove(); + }); + }, 3000); + $(".scrollBottom").fadeOut(300, function() { + unreadedMessages = 0; + me.updateUnreadedMessages(); + }); + } else { + unreadedMessages++; + me.updateUnreadedMessages(); + $(".divisorNewMessages[id!=divisorNewMessages]").find(".newMessages").text(unreadedMessages); + $(".scrollBottom").fadeIn(300).removeClass("hide"); + } + } } - - if (frameless) { - $('.chatList a[id*="deleteMessage"]').each(function(index) { - $(this).attr('onclick', $(this).attr('onclick').replace("'/sakai.chat.deleteMessage.helper","'sakai.chat.deleteMessage.helper")); - }); - - if (deleteUrl.indexOf('/sakai.chat.deleteMessage.helper') == 0) - deleteUrl = deleteUrl.substring(1); + }, + deleteMessages : function (messages) { + for (var i=0; i 0) { + var message = $("#topForm\\:chatList li[data-message-id=" + messageId + "]"); + if (!message.hasClass("nestedMessage") && message.next().hasClass("nestedMessage")) { + message.next().removeClass("nestedMessage"); + } + message.remove(); + chat2_totalMessages--; + chat2_shownMessages--; + this.updateShownText(); + } + } else { + $("#topForm\\:chatList li:not(#chatListItem, #divisorNewMessages)").remove(); + chat2_totalMessages = 0; + chat2_shownMessages = 0; + this.updateShownText(); + } } - - //resize horizontal chat area to get rid of horizontal scrollbar in IE - - var options = { - //RESTful submit URL - url_submit: '/direct/chat-message/new', - control_key: 13, - dom_button_submit_raw: document.getElementById("topForm:controlPanel:submit"), - dom_button_submit: $(document.getElementById("topForm:controlPanel:submit")), - dom_button_reset: $(document.getElementById("topForm:controlPanel:reset")), - dom_textarea: $(document.getElementById("topForm:controlPanel:message")), - channelId: document.getElementById("topForm:chatidhidden").value, - enterKeyCheck:'' - }; - - //Bind button submit action - options.dom_button_submit.bind('click', function() { - options.dom_button_submit_raw.disabled = true; - var params = [{ - name:"chatChannelId", value:options.channelId - },{ - name:"body", value:options.dom_textarea.val() - }]; - if(options.channelId == null || options.channelId == "" || - options.dom_textarea.val() == null || options.dom_textarea.val() == ""){ - options.dom_textarea.focus(); - options.dom_button_submit_raw.disabled = false; - return false; - } - if(options.dom_textarea.val().replace(/\n/g, "").replace(/ /g, "").length == 0){ - options.dom_textarea - .val("") - .focus(); - options.dom_button_submit_raw.disabled = false; - return false; - } - $.ajax({ - url: options.url_submit, - data: params, - type: "POST", - beforeSend: function() { - $("#errorSubmit").slideUp('fast'); - }, - error: function(xhr, ajaxOptions, thrownError) { - $("#errorSubmit").slideDown('fast'); - options.dom_textarea.focus(); - options.dom_button_submit_raw.disabled = false; - return false; - }, - success: function(data) { - //Run dom update from headscripts.js - try { updateNow(); } catch (error) {alert(error);} - options.dom_textarea - .val("") - .focus(); - options.dom_button_submit_raw.disabled = false; - return false; - } - }); - return false; - }); - //Avoid submitting on mouse click in textarea - options.dom_textarea.bind('click', function(){ - return false; - }); - //Bind textarea keypress to submit btn - options.dom_textarea.keydown(function(e){ - var key = e.charCode || e.keyCode || 0; - if( options.control_key == key && !options.dom_button_submit_raw.disabled ){ - options.dom_button_submit.trigger('click'); - return false; - } - }); - options.dom_button_reset.bind('click', function(){ - options.dom_textarea - .val("") - .focus(); - return false; - }); - }); + }, + updatePresentUsers : function (users) { + $("#presence").empty(); + $("#topForm\\:chatList li .chatUserOnline").removeClass("is-online"); + for (var i=0; i -1) { + userId = ownerId.substring(ownerId.indexOf(":") + 1) + } + var userElement = document.createElement("li"); + userElement.setAttribute("data-user-id", ownerId); + userElement.innerHTML = users[i].name; + $("#presence").append(userElement); + $("#topForm\\:chatList li[data-owner-id=" + userId + "] .chatUserOnline").addClass("is-online"); + } + }, + showRemoveModal : function(messageId, ownerDisplayName, date, messageBody) { + var removeModal = $("#removemodal"); + removeModal.find("#owner").text(ownerDisplayName); + removeModal.find("#date").text(date); + removeModal.find("#message").html(messageBody); + removeModal.find("#deleteButton").attr("data-message-id", messageId); + removeModal.modal("show"); + }, + scrollChat : function () { + var scrollableChat = $("#chatListWrapper"); + scrollableChat.scrollTop(scrollableChat.prop("scrollHeight")); + }, + updateShownText : function () { + var countText = chat2_messageCountTemplate + ''; + countText = countText.replace('*SHOWN*', chat2_shownMessages); + countText = countText.replace('*TOTAL*', chat2_totalMessages); + $("#chat2_messages_shown_total").html(countText); + }, + updateUnreadedMessages : function () { + var text = chat2_messagesUnreadedTemplate.replace("*UNREADED*", unreadedMessages); + $(".scrollBottom").attr("title", text); + $(".scrollBottom .newMessages").text(unreadedMessages); + }, + renderDate : function(localizedDate, localizedTime, dateID) { + var msgTime = ""; + if(window.display_date && window.display_time) { + msgTime = " " + localizedDate + " " + localizedTime + " " ; + } else if (window.display_date) { + msgTime = " " + localizedDate + " " ; + } else if(window.display_time) { + msgTime = " " + localizedTime + " " ; + } else if(window.display_id) { + msgTime = " (" + dateID + ") " ; + } + return msgTime; + } +}; diff --git a/chat/chat-tool/tool/src/webapp/jsp/deleteMessageConfirm.jsp b/chat/chat-tool/tool/src/webapp/jsp/deleteMessageConfirm.jsp deleted file mode 100644 index beaf29a28ba5..000000000000 --- a/chat/chat-tool/tool/src/webapp/jsp/deleteMessageConfirm.jsp +++ /dev/null @@ -1,48 +0,0 @@ - - - - - - - - - - - - - - - - - - -
    - - -
    - - - - -
    - - - - -
    - - - - - - - -
    -
    -
    diff --git a/chat/chat-tool/tool/src/webapp/jsp/room.jsp b/chat/chat-tool/tool/src/webapp/jsp/room.jsp index 8e5d6a493343..8b61ccca9003 100644 --- a/chat/chat-tool/tool/src/webapp/jsp/room.jsp +++ b/chat/chat-tool/tool/src/webapp/jsp/room.jsp @@ -1,124 +1,158 @@ - - - - - + + + + + + + + + + + + + + + + + +
    +
    + + + + + + + + + + + + + + + + + + + + +
    - - - - - - - - - - - - - -
    -
    - - - - - - - - - - - - - - - - - - - - -
    + +
    +
    +
    +
    + + + + + + + + +
    +
    + + +
    + <%@ include file="roomMonitor.jspf" %> +
    +
    +
    "> + + +
    +
    +
    +

    + +

    +
      +
    +
    +
    + +
    +
    - -
    -
    -
    - - - - - - - - -
    -
    - - -
    - <%@ include file="roomMonitor.jspf" %> -
    -
    -
    -

    - -

    -
      -
    -
    -
    - -
    -
    + + + + +

    - - - - -

    + - - - -
    + + + +
    diff --git a/chat/chat-tool/tool/src/webapp/jsp/roomMonitor.jspf b/chat/chat-tool/tool/src/webapp/jsp/roomMonitor.jspf index fbc9d29dbda7..8ccb9ef501fd 100644 --- a/chat/chat-tool/tool/src/webapp/jsp/roomMonitor.jspf +++ b/chat/chat-tool/tool/src/webapp/jsp/roomMonitor.jspf @@ -1,84 +1,90 @@ - - -
      - - - - - - - - - - " escapeXml="false"/> - - - - - - - - ${message.dateTime}" escapeXml="false" /> - - - - - ${message.date}" escapeXml="false" /> - - - - - ${message.time}" escapeXml="false" /> - - - - - (${message.id})" escapeXml="false" /> - - - - - - - - - - " escapeXml="false" /> - - - - - - +
    • + + + + + + + + + + + + + " escapeXml="false" /> +
    • +
    • + + ${msgs['new_messages']}" escapeXml="false" /> +
    • + + + + + + + + + + " escapeXml="false"/> + + + + + + + + ${message.dateTime}" escapeXml="false" /> + + + + + ${message.date}" escapeXml="false" /> + + + + + ${message.time}" escapeXml="false" /> + + + + + (${message.id})" escapeXml="false" /> + + + + + + + + + + + + + + +
    diff --git a/chat/chat-tool/tool/src/webapp/jsp/roomUsers.jsp b/chat/chat-tool/tool/src/webapp/jsp/roomUsers.jsp deleted file mode 100644 index c5af36bcfe45..000000000000 --- a/chat/chat-tool/tool/src/webapp/jsp/roomUsers.jsp +++ /dev/null @@ -1,5 +0,0 @@ - - -
  • -
    -
    diff --git a/chat/pom.xml b/chat/pom.xml index d92d02b85786..b2bf43a124d5 100644 --- a/chat/pom.xml +++ b/chat/pom.xml @@ -32,6 +32,16 @@ sakai-chat-impl ${project.version}
    + + org.jgroups + jgroups + 3.6.13.Final + + + com.google.guava + guava + 22.0 + diff --git a/chat/readme.md b/chat/readme.md new file mode 100644 index 000000000000..d00bec45d4e6 --- /dev/null +++ b/chat/readme.md @@ -0,0 +1,35 @@ +# Chat +https://jira.sakaiproject.org/browse/SAK-32035 + +## Changes +* Removed some cover uses and replaced by spring injection +* Added lombok +* New Data structures based on portal-chat + - Hearbeat (MAP[CHANNEL_ID][SESSION_ID] -> TransferableChatMessage) : This map will store who is alive in each channel. We store the session_id to allow login multiple times with different browsers. This data structure uses Guava cache to automatically expire data when no accessed/writted. The logic is : + - All channel_id related data will be removed if nobody reads it in pollIntervalx2 time. + - All session_id related data will be removed if nobody writes it in pollIntervalx2 time. (When adding a heartbeat, this data will be written). + - MessageMap (MAP[SESSION_KEY][CHANNEL_ID] -> List) : This map will store all messages pending to deliver for each session_key (ussage_session_id:session_user_id) in each channel. This data structure uses Guava cache to automatically expire data when no accessed. If nobody access to a given session_key in 5 minutes, that data will be removed (We assume the pollInterval will occur much much often than 5 minutes and that data will be accessed, consumed and manually removed). This just make us sure that messages sended and not delivered (because the user has left the channel/tool) will be automatically deleted. +* New custom action "chatData" added to ChatMessageEntityProvider : this action will sustitute the current courier. + - This action is supposed to be called every pollInterval time. + - When called, update the heartbeat for the caller (if I'm asking for data, this means I'm alive). + - Return undelivered messages for the current user and clean them all (once consumed we do not need them anymore). Messages can be : chat message or remove message. + - Get all present and online users in the given channel. +* Added new fields to "SimpleChatMessage" (entity used by ChatMessageEntityProvider) : boolean removeable, MessageDateString messageDateString (String pdate, String ptime, String pid). Also upgraded with lombok. +* Using jGroups to communicate cluster nodes. jGroups messages allow us to synchronize the data structures (heartbeat + messageMap) between nodes. +* Added a default jGroups config xml file (**jgroups-config.xml**). This file is provided with the tool and will avoid problems with portal chat default configuration (uses a different multicast IP than the default). Anyway, this file can be overwritten setting a file called **jgroups-chat-config.xml** in SAKAI_HOME folder. +* Removed courier references : All uses of the courier service have been removed. +* Removed EventTracking and Presence Observers : We are not listening anymore the EventTracking or Presence events. These events where used to synchonize cluster nodes. This is not needed because of jGroups. +* Removed custom action "listen" from ChatChannelEntityProvider. This action was used to set a channel listener (appart from the used by the chat). Now, you can listen any channel by simply calling periodically "chatData" (of course you need the right permissions to do that). +* Improved GUI : Auto-scroll feature + +## New Sakai properties +* chat.pollInterval (default 5000) : Time (in milliseconds) between each AJAX poll. This is also used to automatically expire guava cache data. +* chat.cluster.channel : jGroups channel id where the messages are going to be delivered. CARE: at this point, this value must be different from "portalchat.cluster.channel". + +## Important JVM property +Make sure the java property **-Djava.net.preferIPv4Stack=true** is set. If not, jGroups may have problems delivering messages through network. + +## Future improvements +- Move "sendToCluster" function to an external service like MesageService. +- Create a wrapper class in an external service (like MesageService). This class will be used to send generic messages between nodes in a cluster using jGroups. All tools/services who want to be synchonized in the cluster must extend this wrapper class. In our case, we will do this for Chat::transferableChatMessage and PortalChat::UserMessage +- Use the same jGroups channel for all tools/services (thanks to the wrapper class). This will remove some specific properties like portalchat.cluster.channel and chat.cluster.channel \ No newline at end of file diff --git a/library/src/morpheus-master/sass/modules/tool/chat/_chat.scss b/library/src/morpheus-master/sass/modules/tool/chat/_chat.scss index b7488d1b28ed..aac6a8022e74 100644 --- a/library/src/morpheus-master/sass/modules/tool/chat/_chat.scss +++ b/library/src/morpheus-master/sass/modules/tool/chat/_chat.scss @@ -1,15 +1,12 @@ ////////////////////////////// // MODULES: Styles that are layout independent ////////////////////////////// - @import "_chat_variables"; - .Mrphs-{ &sakai-chat{ .panel-chat { padding: 0; margin: 14px 0 0 0; - .panel-heading { h3 { margin: 0; @@ -28,7 +25,6 @@ } } } - .nav.nav-tabs { margin: 10px 0 0 0; border-color: #dddddd; @@ -36,8 +32,12 @@ @media #{$tablet} { display: block; } + li { + a { + padding: 10px 9px; + } + } } - .panel-body-chat { display: -webkit-box; display: -moz-box; @@ -49,151 +49,192 @@ -webkit-flex-flow: column-reverse; flex-flow: column-reverse; } - - .chatListWrapper { + .chatListWrapperCont { width: 100%; max-height: 517px; - overflow-y: scroll; - @media #{$desktop} { - display: block; - } - @media #{$tablet} { - width: 100%; - } - - .chatListHeadWrapper { - padding: 8px 17px 0; - color: #888888; - text-align: center; - } - - .shown_total { - color: #888888; - text-align: center; - margin: 0 17px 0 17px; - padding-bottom: 8px; - } - - .alertMessage { - margin: 10px; - } - - .chatListMonitor { - border: 0 !important; - padding: 0 !important; - - .chatList { - list-style: none; - padding: 0; - margin: 0; - - li { - margin: 0 17px; - padding: 19px 0 5px 0; - border-top: 1px solid #f1f1f1; - position: relative; - &.prev { - padding: 0 0 5px 0; - border-top: 0; - - .chatUserAvatar, .chatNameDate { - display: none; - } + position: relative; - .chatMessage { - margin-top: 0; - .chatMessageDate { + .chatListWrapper { + width: 100%; + height: 100%; + max-height: 517px; + overflow-y: scroll; + @media #{$desktop} { + display: block; + } + @media #{$tablet} { + width: 100%; + } + .chatListHeadWrapper { + padding: 8px 17px 0; + color: #888888; + text-align: center; + } + .shown_total { + color: #888888; + text-align: center; + margin: 0 17px 0 17px; + padding-bottom: 8px; + } + .alertMessage { + margin: 10px; + } + .chatListMonitor { + border: 0 !important; + padding: 0 !important; + .chatList { + list-style: none; + padding: 0; + margin: 0; + li { + margin: 0 17px; + padding: 19px 0 5px 0; + border-top: 1px solid #f1f1f1; + position: relative; + &.nestedMessage { + padding: 0 0 5px 0; + border-top: 0; + .chatUserAvatar, .chatNameDate { display: none; } + .chatMessage { + margin-top: 0; + .chatMessageDate { + display: none; + } + } } - } - - .chatUserAvatar { - position: absolute; - top: 13px; - left: 0; - margin-right: 8px; - display: inline-block; - width: 42px; - height: 42px; - border-radius: 21px; - line-height: 40px; - color: #fff; - text-align: center; - font-weight: bold; - font-size: 15px; - text-transform: uppercase; - background-size: cover; - background-repeat: no-repeat; - background-position-x: center; - - .chatUserOnline { - background: #8BC34A; - height: 12px; - width: 12px; - position: absolute; - bottom: -2px; - border-radius: 6px; - right: -2px; - display: none; + &.divisorNewMessages:not(.hide) { + margin: 0 0 5px 0; + padding: 5px 17px; + text-align: center; + border-bottom: 1px solid #f1f1f1; + background: #f7f7f7; + + li { + border-top: 0; + } - &.is-online { - display: block; + .newMessages { + background: $link-color; + border-radius: 50px; + padding: 2px 7px; + font-size: 10px; + color: #fff; + margin-right: 5px; } } - } - .chatNameDate { - margin-left: 54px; - line-height: 13px; - display: block; - - .chatName { + .chatUserAvatar { + position: absolute; + top: 13px; + left: 0; + margin-right: 8px; display: inline-block; + width: 42px; + height: 42px; + border-radius: 21px; + line-height: 40px; + color: #fff; + text-align: center; font-weight: bold; - margin-right: 4px - } - - .chatDate { - font-size: 9px; - color: #cacaca; - display: inline-block; - margin-right: 32px; - } - } - - .chatMessage { - display: block; - color: #656565; - position: relative; - line-height: 19px; - margin: 5px 0 5px 53px; - - .chatMessageDate { - font-size: 9px; - color: #cacaca; - position: absolute; - top: -13px; - display: none; + font-size: 15px; + text-transform: uppercase; + background-size: cover; + background-repeat: no-repeat; + background-position-x: center; + .chatUserOnline { + background: #8BC34A; + height: 12px; + width: 12px; + position: absolute; + bottom: -2px; + border-radius: 6px; + right: -2px; + display: none; + &.is-online { + display: block; + } + } } - - .chatText { - padding-right: 32px; - word-wrap: break-word; + .chatNameDate { + margin-left: 54px; + line-height: 13px; display: block; + .chatName { + display: inline-block; + font-weight: bold; + margin-right: 4px + } + .chatDate { + font-size: 9px; + color: #cacaca; + display: inline-block; + margin-right: 32px; + } } - - .chatRemove { - font-size: 20px; - position: absolute; - top: 0; - right: 8px; + .chatMessage { + display: block; + color: #656565; + position: relative; + line-height: 19px; + margin: 5px 0 5px 53px; + .chatMessageDate { + font-size: 9px; + color: #cacaca; + position: absolute; + top: -13px; + display: none; + } + .chatText { + padding-right: 32px; + word-wrap: break-word; + display: block; + } + .chatRemove { + font-size: 20px; + position: absolute; + top: 0; + right: 8px; + cursor: pointer; + color: $link-color; + } } } } } } - } + .scrollBottom { + position: absolute; + bottom: 0; + left: 50%; + margin: 17px; + transform: translate(-50%); + max-width: 500px; + text-align: center; + width: 40px; + height: 40px; + border-radius: 20px; + background: white; + border: 1px solid #dcdcdc; + cursor: pointer; + + .newMessages { + position: absolute; + top: -10px; + background: $link-color; + border-radius: 50px; + padding: 2px 7px; + font-size: 10px; + transform: translate(-50%); + left: 50%; + color: #fff; + } + + .scrollIcon { + line-height: 39px; + } + } + } .chatListOnline { width: 300px; border-left: 1px solid #f1f1f1; @@ -207,7 +248,6 @@ padding: 10px 0; width: 100%; } - .chatListHeadWrapper { margin: 0 10px 0 10px; padding: 8px 0 5px 0; @@ -218,13 +258,11 @@ display: none; } } - .presenceList { list-style: none; li { padding: 5px 17px 0 17px; line-height: 15px; - &:last-of-type { padding-bottom: 5px; } @@ -232,14 +270,12 @@ } } } - .panel-footer { #topForm\:controlPanel\:message { width: 100%; border: 1px solid #dddddd; padding: 2px 5px; } - .act { padding: 5px 0 0 0; } @@ -260,6 +296,28 @@ margin: 0 10px; } } + + #removemodal { + .modal-header { + .close { + padding-top: 6px; + } + } + .modal-body { + th { + vertical-align: top; + padding-right: 5px; + } + td { + word-break: break-word; + } + } + .modal-footer { + .btn { + margin-bottom: 0.5em; + } + } + } } &footerApp{ &__presence, &__portalChat{