/* * Copyright (C) 2004-2006 Jive Software. All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.jivesoftware.xmpp.workgroup.request; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.SQLException; import java.util.Arrays; import java.util.Date; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Queue; import java.util.concurrent.ConcurrentLinkedQueue; import org.dom4j.DocumentHelper; import org.dom4j.Element; import org.dom4j.QName; import org.jivesoftware.database.DbConnectionManager; import org.jivesoftware.util.NotFoundException; import org.jivesoftware.util.StringUtils; import org.jivesoftware.xmpp.workgroup.AgentSession; import org.jivesoftware.xmpp.workgroup.RequestQueue; import org.jivesoftware.xmpp.workgroup.UserCommunicationMethod; import org.jivesoftware.xmpp.workgroup.Workgroup; import org.jivesoftware.xmpp.workgroup.chatbot.ChatbotSession; import org.jivesoftware.xmpp.workgroup.spi.WorkgroupCompatibleClient; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.xmpp.forms.DataForm; import org.xmpp.forms.FormField; import org.xmpp.packet.IQ; import org.xmpp.packet.JID; /** * Requests made by users to get support by some agent. * * @author Gaston Dombiak */ public class UserRequest extends Request { private static final Logger Log = LoggerFactory.getLogger(UserRequest.class); private static final String INSERT_SESSION = "INSERT INTO fpSession(sessionID, userID, workgroupID, state, queueWaitTime, " + "startTime, endTime) values(?,?,?,?,?,?,?)"; private static final String UPDATE_SESSION = "UPDATE fpSession SET state=?, queueWaitTime=?, endTime=? WHERE sessionID=?"; private static final String INSERT_META_DATA = "INSERT INTO fpSessionMetadata(sessionID, metadataName, metadataValue) VALUES(?,?,?)"; /** * Flag that indicates if an invitation that was not answered was offered again to the user. * The way in which the offer will be offered again to the user may vary according to the type * of client used by the user. For instance, if the user is using a chatbot to join the * workgroup then a new message may be sent to the user asking if he wants to receive a new * offer or cancel the request. */ private boolean invitationChecked = false; private JID userJID; private String userID; private boolean anonymousUser; /** * Keeps the object that represents the type of client that the user is using. */ private UserCommunicationMethod communicationMethod; private RequestQueue queue = null; private boolean savedToDB = false; /** * Timestamp that indicates when an invitation was sent to the user that made this request. */ private long invitationSent; /** * ID of the room where the user was invited. */ private String invitedRoomID; /** * Requests that are related to a user request. For instance, an invitation request sent to an * agent that is related to this user request. */ private Queue<Request> relatedRequests = new ConcurrentLinkedQueue<Request>(); /** * Returns an existing request given the requesting user's address and workgroup or throws * NotFoundException if none was found. * * @param workgroup the workgroup that the user us trying to join. * @param address the address to check. * @return a request given the requesting user's address and workgroup. * @throws org.jivesoftware.util.NotFoundException if the request could not be found. */ public static UserRequest getRequest(Workgroup workgroup, JID address) throws NotFoundException { UserRequest request = null; for (RequestQueue requestQueue : workgroup.getRequestQueues()) { if (request == null) { request = requestQueue.getRequest(address); } } if (request == null) { Log.debug("Request not found for " + address.toString()); throw new NotFoundException(); } return request; } public UserRequest(IQ packet, Workgroup wg) { super(); this.userJID = packet.getFrom(); this.userID = userJID.toBareJID(); this.anonymousUser = false; this.workgroup = wg; // Requests to join a workgroup made using an IQ are assumed to be using a Workgroup // compatible client this.communicationMethod = WorkgroupCompatibleClient.getInstance(); Iterator<Element> elementIter = packet.getChildElement().elementIterator(); while (elementIter.hasNext()) { Element element = elementIter.next(); if ("queue-notifications".equals(element.getName())) { setNotify(true); } else if ("user".equals(element.getName())) { userID = element.attributeValue("id"); if (userID != null && userID.length() > 0) { anonymousUser = !userJID.toString().equals(userID) && !userJID.toBareJID().equals(userID); } } else if ("metadata".equals(element.getName())) { for (Iterator<Element> i = element.elementIterator(); i.hasNext();) { Element item = i.next(); if ("value".equals(item.getName())) { String name = item.attributeValue("name"); if (name != null) { metaData.put(name, Arrays.asList(item.getTextTrim())); } } } } } // Create metadata from submitted form. DataForm submittedForm = (DataForm)packet.getExtension(DataForm.ELEMENT_NAME, DataForm.NAMESPACE); for (FormField field : submittedForm.getFields()) { metaData.put(field.getVariable(), field.getValues()); } // Omit certain items metaData.remove("password"); } /** * Creates a new request made by a user using a chatbot as the communication media. * * @param session the chatbot session that holds all the information sent by the user. * @param wg the workgroup that the user wants to join. */ public UserRequest(ChatbotSession session, Workgroup wg) { super(); this.userJID = session.getUserJID(); this.userID = userJID.toBareJID(); this.anonymousUser = false; this.workgroup = wg; // Use the chatbot of the session as the method to communicate with the user that // made the request this.communicationMethod = session.getChatbot(); // Always set that users using a bot want to be notified of the position in the queue setNotify(true); // Use the stored attributes in the session as the metadata of the request metaData.putAll(session.getAttributes()); // Make sure that the following keys are present in the metadata if (!metaData.containsKey("userID")) { metaData.put("userID", Arrays.asList(userJID.toString())); } } /** * Update the current position of the user in the queue. This will send * the packet directly to the user. * * @param isPolling true if using polling mode. */ public void updateQueueStatus(boolean isPolling) { try { // Notify the user his status in the queue communicationMethod.notifyQueueStatus(workgroup.getJID(), userJID, this, isPolling); } catch (Exception e) { Log.error(e.getMessage(), e); } } @Override public void checkRequest(String roomID) { if (getInvitationSentTime() != null && !hasJoinedRoom()) { checkInvitation(roomID); } for (Request request : relatedRequests) { request.checkRequest(roomID); } } public void setRequestQueue(RequestQueue queue) { this.queue = queue; } public RequestQueue getRequestQueue() { return queue; } /** * Returns the ID of the room where the user was invited to have a chat with an agent or * <tt>null</tt> if the user was never invited. * * @return the ID of the room where the user was invited to have a chat with an agent or * <tt>null</tt> if the user was never invited. */ public String getInvitedRoomID() { return invitedRoomID; } /** * Returns the Date when an invitation to join a room was sent to the user that made this * request. Or answer <tt>null</tt> if an invitation was never sent. * * @return the Date when an invitation to join a room was sent to the user that made this * request. */ public Date getInvitationSentTime() { if (invitationSent > 0) { return new Date(invitationSent); } return null; } public int getPosition() { int pos = -1; if (queue != null) { pos = queue.getPosition(this); } return pos; } public int getTimeStatus() { int averageTime = queue == null ? 0 : queue.getAverageTime(); int timeStatus; if (averageTime == 0) { timeStatus = (getPosition() + 1) * 15; } else { timeStatus = (getPosition() + 1) * averageTime; } return timeStatus; } @Override public JID getUserJID() { return userJID; } /** * Returns the user unique identification. If the user joined using an anonymous connection * then the userID will be the value of the ID attribute of the USER element. Otherwise, the * userID will be the bare JID of the user that made the request. * * @return the user unique identification. */ public String getUserID() { return userID; } /** * Returns true if the request was made by a user that is using an anonymous session. For * anonymous user the userJID will be something like "server.com/a9d32h" and will vary * each time the user creates a new connection whilst the userID will represent the user * unique identification that may be used across several sessions. * * @return true if the request was made by a user that is using an anonymous session. */ public boolean isAnonymousUser() { return anonymousUser; } /** * The request is being asked to verify if a new invitation must be sent to the user that * didn't answer the previous invitation. The request will only retry one more time. * * @param roomID the id of the room where the user was invited. */ public void checkInvitation(String roomID) { if (!invitationChecked && !hasJoinedRoom() && System.currentTimeMillis() - invitationSent > 10000) { invitationChecked = true; communicationMethod.checkInvitation(this); } } /** * Notification message saying that the request has been accepted by an agent and that * invitations have been sent to the agent and the user that made the request. * * @param roomID the id of the room for which invitations were sent. */ public void invitationsSent(String roomID) { invitationSent = System.currentTimeMillis(); invitedRoomID = roomID; communicationMethod.invitationsSent(this); } /** * Notification message saying that the user that made the request has joined the room to have * a chat with an agent. * * @param roomID the id of the room where the user has joined. */ public void supportStarted(String roomID) { joinedRoom = System.currentTimeMillis(); communicationMethod.supportStarted(this); } /** * Notification message saying that the support session has finished. At this point all room * occupants have left the room. */ public void supportEnded() { communicationMethod.supportEnded(this); } @Override public void userJoinedRoom(JID roomJID, JID user) { // Notify related requests that new a occupant has joined the room for (Request request : relatedRequests) { request.userJoinedRoom(roomJID, user); } } /** * Adds a request that is somehow related to this user request. This is usually the case when * an invitation or transfer was sent to another agent. For these cases a new Request is generated * that is related to this request. Since all these requests will be interested in the room activitiy * we need to propagate support events (i.e. supportStarted and supportEnded). * * @param request the request that is related to this request. */ public void addRelatedRequest(Request request) { relatedRequests.add(request); } /** * Remvoes a request that is no longer related to this user request. * * @param request the request that is no longer related to this request. */ public void removeRelatedRequest(Request request) { relatedRequests.remove(request); } @Override public void cancel(Request.CancelType type) { super.cancel(type); JID sender = workgroup.getJID(); if (queue != null) { sender = queue.getWorkgroup().getJID(); queue.removeRequest(this); } try { // Notify the user that he has left the queue communicationMethod.notifyQueueDepartued(sender, userJID, this, type); } catch (Exception e) { Log.error(e.getMessage(), e); } } @Override void addOfferContent(Element offerElement) { // Flag the offer as a user request offerElement.addElement("user-request"); // Add custom extension that includes the userID if the session belongs to an // anonymous user if (isAnonymousUser()) { Element element = offerElement.addElement("user", "http://jivesoftware.com/protocol/workgroup"); element.addAttribute("id", getUserID()); } } @Override void addRevokeContent(Element revoke) { // Add custom extension that includes the userID if the session belongs to an // anonymous user if (isAnonymousUser()) { Element element = revoke.addElement("user", "http://jivesoftware.com/protocol/workgroup"); element.addAttribute("id", getUserID()); } } @Override public Element getSessionElement() { QName qName = DocumentHelper.createQName("session", DocumentHelper.createNamespace("", "http://jivesoftware.com/protocol/workgroup")); Element sessionElement = DocumentHelper.createElement(qName); sessionElement.addAttribute("id", requestID); sessionElement.addAttribute("workgroup", getWorkgroup().getJID().toString()); return sessionElement; } /** * Sends an invitation to the agent that previously accepted the offer to join a room. * Agents need to join a room to be able to chat (and fulfil the request) with the * user that sent the request. * * @param agentSession the agent that previously accepted the offer. */ @Override public void offerAccepted(AgentSession agentSession) { super.offerAccepted(agentSession); // Ask the workgroup to send invitations to the agent and to the user that made the // request. The Workgroup will create a MUC room and send invitations to the agent and // the user. getWorkgroup().sendInvitation(agentSession, this); } @Override public void updateSession(int state, long offerTime) { boolean inserted = false; long queueWaitTime = new Date().getTime() - offerTime; String tempDate = StringUtils.dateToMillis(new Date()); // Gather all information needed. Connection con = null; PreparedStatement pstmt = null; try { con = DbConnectionManager.getConnection(); synchronized (this) { if (!savedToDB) { pstmt = con.prepareStatement(INSERT_SESSION); pstmt.setString(1, requestID); pstmt.setString(2, getUserID()); pstmt.setLong(3, getWorkgroup().getID()); pstmt.setInt(4, state); pstmt.setLong(5, queueWaitTime); pstmt.setString(6, tempDate); pstmt.setString(7, tempDate); pstmt.executeUpdate(); savedToDB = true; inserted = true; } else { pstmt = con.prepareStatement(UPDATE_SESSION); pstmt.setInt(1, state); pstmt.setLong(2, queueWaitTime); pstmt.setString(3, tempDate); pstmt.setString(4, requestID); pstmt.executeUpdate(); } } } catch (Exception ex) { Log.error( "There was an issue handling offer update using sessionID " + requestID, ex); } finally { DbConnectionManager.closeConnection(pstmt, con); } if (inserted) { saveMetadata(); } } private void saveMetadata() { final Map<String, List<String>> map = getMetaData(); Connection con = null; try { con = DbConnectionManager.getConnection(); PreparedStatement pstmt = con.prepareStatement(INSERT_META_DATA); for (String key : map.keySet()) { List<String> values = map.get(key); pstmt.setString(1, requestID); pstmt.setString(2, key); pstmt.setString(3, encodeMetadataValue(values)); pstmt.executeUpdate(); } pstmt.close(); } catch (SQLException e) { Log.error(e.getMessage(), e); } finally { DbConnectionManager.closeConnection(con); } } }