/*
* Copyright (C) 2004-2008 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.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.StringTokenizer;
import java.util.concurrent.ConcurrentHashMap;
import org.dom4j.DocumentHelper;
import org.dom4j.Element;
import org.dom4j.QName;
import org.jivesoftware.database.SequenceManager;
import org.jivesoftware.util.NotFoundException;
import org.jivesoftware.util.StringUtils;
import org.jivesoftware.xmpp.workgroup.AgentSession;
import org.jivesoftware.xmpp.workgroup.Offer;
import org.jivesoftware.xmpp.workgroup.RequestQueue;
import org.jivesoftware.xmpp.workgroup.Workgroup;
import org.jivesoftware.xmpp.workgroup.utils.FastpathConstants;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.xmpp.packet.IQ;
import org.xmpp.packet.JID;
/**
* <p>Database compatible workgroup request information.</p>
*
* @author Derek DeMoro
*/
public abstract class Request {
private static final Logger Log = LoggerFactory.getLogger(Request.class);
private static final Map<String, Request> requests = new ConcurrentHashMap<String, Request>();
protected final String requestID;
private Date creationTime;
/**
* Timestamp that indicates when the last user joined the support MUC room.
*/
protected long joinedRoom;
/**
* Workgroup that issued the offer.
*/
protected Workgroup workgroup;
private boolean notify = false;
protected Offer offer;
protected Map<String, List<String>> metaData = new HashMap<String, List<String>>();
private static String newRequestID() {
// Create a requestID for the new request
long requestCounter = SequenceManager.nextID(FastpathConstants.WORKGROUP_QUEUE);
String requestID = (StringUtils.randomString(6) + Long.toString(requestCounter)).toLowerCase();
// Replace possible white spaces with the '_' character
return requestID.replace(' ', '_');
}
/**
* Returns an existing request based on it unique ID. If none was found then a NotFoundException
* exception will be thrown.<p>
*
* Requests are tracked once an initial offer has been sent to an agent. Tracking of requests is
* stopped once the request was cancelled or accepted.
*
* @param requestID the unique identifier of the request.
* @return an existing request based on it unique ID.
* @throws org.jivesoftware.util.NotFoundException if the request could not be found.
*/
public static Request getRequest(String requestID) throws NotFoundException {
Request request = requests.get(requestID);
if (request == null) {
Log.debug("Request not found by ID: " + requestID);
throw new NotFoundException();
}
return request;
}
protected Request() {
creationTime = new Date();
requestID = newRequestID();
}
public Offer getOffer() {
return offer;
}
public void setOffer(Offer offer) {
this.offer = offer;
}
public void setNotify(boolean notify) {
this.notify = notify;
}
public boolean isNotify() {
return notify;
}
public Date getCreationTime() {
return creationTime;
}
public String getSessionID() {
return requestID;
}
/**
* Send a Cancel notice to the queue when a user has left.
*
* @param type the type of Cancel. The type will be used to explain the reason of the departure.
* @see CancelType
*/
public void cancel(Request.CancelType type) {
// Stop tracking this request
requests.remove(requestID);
if (offer != null) {
offer.cancel();
}
}
/**
* Returns true if the user that made this request joined a room to have a chat with an agent.
*
* @return true if the user that made this request joined a room to have a chat with an agent.
*/
public boolean hasJoinedRoom() {
return joinedRoom > 0;
}
/**
* Returns the Date when the user joined the room to have a chat with an agent. Or answer
* <tt>null</tt> if the user never joined a chat room.
*
* @return the Date when the user joined the room to have a chat with an agent.
*/
public Date getJoinedRoomTime() {
if (joinedRoom > 0) {
return new Date(joinedRoom);
}
return null;
}
public abstract Element getSessionElement();
public Workgroup getWorkgroup() {
return workgroup;
}
public Map<String, List<String>> getMetaData() {
return metaData;
}
public Element getMetaDataElement() {
QName qName = DocumentHelper.createQName("metadata", DocumentHelper.createNamespace("",
"http://jivesoftware.com/protocol/workgroup"));
Element metaDataElement = DocumentHelper.createElement(qName);
for (String name : metaData.keySet()) {
List<String> values = metaData.get(name);
for (String value : values) {
Element elem = metaDataElement.addElement("value");
elem.addAttribute("name", name).setText(value);
}
}
return metaDataElement;
}
/**
* Updates the session table with the new session state and queueWaitTime.
*
* @param state the state of this session.
* @param offerTime the new offer time.
*/
public abstract void updateSession(int state, long offerTime);
/**
* Notification event indicating that an agent has accepted the offer. Subclasses should
* react accordingly (e.g. create a room and send invitations to agent and user that made the request).
*
* @param agentSession the agent that previously accepted the offer.
*/
public void offerAccepted(AgentSession agentSession) {
// Stop tracking this request
requests.remove(requestID);
}
/**
* Sends an offer to the specified agent for this request. The agent may accept or reject
* the offer. Moreover, the offer may be revoked while the agent hasn't answered it. This
* may happen for several reasons: request was cancelled, offer timed out, etc.
*
* @param session the agent that will get the offer.
* @param queue queue that is sending the offer.
* @return true if the offer was sent to the agent.
*/
public boolean sendOffer(AgentSession session, RequestQueue queue) {
// Keep track of this request by its ID
requests.put(requestID, this);
// Create new IQ to Send
IQ offerPacket = new IQ();
offerPacket.setFrom(queue.getWorkgroup().getJID());
offerPacket.setTo(session.getJID());
offerPacket.setType(IQ.Type.set);
Element offerElement = offerPacket.setChildElement("offer", "http://jabber.org/protocol/workgroup");
offerElement.addAttribute("id", requestID);
offerElement.addAttribute("jid", getUserJID().toString());
Element metaDataElement = getMetaDataElement();
offerElement.add(metaDataElement);
Element timeoutElement = offerElement.addElement("timeout");
timeoutElement.setText(Long.toString(offer.getTimeout() / 1000));
offerElement.add(getSessionElement());
addOfferContent(offerElement);
return session.sendOffer(offer, offerPacket);
}
/**
* Revokes an offer that was previously sent to an agent.
*
* @param session the agent session that will get the revoke.
* @param queue queue that is sending the offer.
*/
public void sendRevoke(AgentSession session, RequestQueue queue) {
IQ agentRevoke = new IQ();
agentRevoke.setFrom(queue.getWorkgroup().getJID());
agentRevoke.setTo(session.getJID());
agentRevoke.setType(IQ.Type.set);
Element revoke = agentRevoke.setChildElement("offer-revoke", "http://jabber.org/protocol/workgroup");
revoke.addAttribute("id", requestID);
revoke.addAttribute("jid", getUserJID().toString());
revoke.addElement("reason").setText("The offer has timed out");
revoke.add(getSessionElement());
addRevokeContent(revoke);
session.sendRevoke(offer, agentRevoke);
}
abstract void addOfferContent(Element offerElement);
abstract void addRevokeContent(Element revoke);
abstract JID getUserJID();
public abstract void checkRequest(String roomID);
/**
* Notification message indicating that someone has joined the room where the support
* session is taking place.
*
* @param roomJID the jid of the room where the occupant has joined.
* @param user the JID of the user that joined the room.
*/
public abstract void userJoinedRoom(JID roomJID, JID user);
public enum CancelType {
/**
* An agent was not found so the user that sent the offer will be removed from the queue.
*/
AGENT_NOT_FOUND("agent-not-found"),
/**
* The user requested to depart the queue or an administrator asked to remove the user from
* the queue.
*/
DEPART("departure-requested");
private String description;
CancelType(String description) {
this.description = description;
}
public String getDescription() {
return description;
}
}
/**
* Returns a string representation of a list of strings. Each string contained in the
* collection will be separated by a '/' character.
*
* @param values the collection of strings to encode.
* @return a string representation of a list of strings.
*/
public static String encodeMetadataValue(List<String> values) {
StringBuilder builder = new StringBuilder();
for (Iterator<String> it = values.iterator(); it.hasNext();) {
builder.append(it.next());
if (it.hasNext()) {
builder.append("/");
}
}
return builder.toString();
}
/**
* Returns a collection of strings from an encoded string. The encoded string used a '/'
* character as a separator between each value.
*
* @param values the encoded string where the values are separated by '/'.
* @return a collection of strings from an encoded string.
*/
public static List<String> decodeMetadataValue(String values) {
List<String> answers = new ArrayList<String>();
StringTokenizer tokenizer = new StringTokenizer(values, "/");
while (tokenizer.hasMoreTokens()) {
answers.add(tokenizer.nextToken());
}
return answers;
}
}