/*
* 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;
import org.jivesoftware.xmpp.workgroup.request.Request;
import java.util.*;
import java.util.concurrent.CopyOnWriteArrayList;
/**
* <p>A simple, in-memory implementation of an Offer.</p>
* <p/>
* <p>Offers are not designed to survive the shutdown of the server.</p>
*
* @author Derek DeMoro
*/
public class Offer {
/**
* User associated with this Offer
*/
private Request request;
/**
* RequestQueue that is sending this offer to the queue's agents.
*/
private RequestQueue queue;
/**
* AgentSessions that received this offer and have not yet answered. Once the invitation has
* been sent this list will be cleared.
*/
private List<AgentSession> pendingSessions = new CopyOnWriteArrayList<AgentSession>();
/**
* AgentSessions that accepted this offer and one of them will be choosen to start a
* conversation. Once the invitation has been sent this list will be cleared.
*/
private List<AgentSession> acceptedSessions = new CopyOnWriteArrayList<AgentSession>();
/**
* The (server) time the Offer was made to the agent, initialized in constructor.
*/
private Date offerTime;
/**
* The timeout of the Offer in milliseconds or -1 for no timeout.
*/
private long timeout = 20000;
/**
* The set of agents that have rejected this Offer.
*/
private Set<String> rejections = new HashSet<String>();
// ########################################################################
/**
* <p>Temporary auto re-offer timeouts for rejectors.</p>
* <p/>
* <p>Matt wants agent rejectors to be automatically re-offered
* offers after a timeout regardless of the agent's actions.
* The motivation is to ensure offers don't stay idle in the queue
* waiting for an agent to change their status to get another offer.</p>
* <p/>
* <p>The proper behavior shoud be to have the agent app notifyEvent the server
* when the agent is ready to accept new offers (or be re-offered old ones)
* at user defined times. Manually is probably preferred (press button on
* app to indicate "ready for next". Or could be automatic via a timer.
* In either case driving this from the server is a poor solution
* and should be fixed ASAP.</p>
*/
private Map<String, Date> rejectionTimes = new HashMap<String, Date>();
/**
* The length in milliseconds before automatically removing an agent
* from the rejector list.
*/
private long rejectionTimeout;
/**
* Flag indicating the offer has been cancelled or not.
*/
private boolean cancelled;
/**
* Flag indicating an invitation was sent to an agent or not. An invitation will be sent after
* an agent accepted the offer and was chosen to atend the user's request.
*/
private boolean invitationSent;
/**
* Defined States *
*/
public static final int USER_CANCELLED = 0;
public static final int ROUTE_EXPIRED = 1;
public static final int ROUTED = 2;
/**
* Create an offer based on the request.
*
* @param request The request this offer is for
* @param queue The request queue that is sending this offer to the agents.
* @param rejectionTimeout the number of milliseconds to wait until expiring an agent rejection.
*/
public Offer(Request request, RequestQueue queue, long rejectionTimeout) {
this.request = request;
this.queue = queue;
this.rejectionTimeout = rejectionTimeout;
offerTime = new Date();
cancelled = false;
invitationSent = false;
request.setOffer(this);
}
public Request getRequest() {
return request;
}
public boolean isAccepted() {
return !acceptedSessions.isEmpty();
}
public boolean isCancelled() {
return cancelled;
}
public void accept(AgentSession agentSession) {
acceptedSessions.add(agentSession);
pendingSessions.remove(agentSession);
}
public void reject(AgentSession agentSession) {
if (pendingSessions.contains(agentSession)) {
addRejector(agentSession);
pendingSessions.remove(agentSession);
agentSession.removeOffer(this);
}
}
private void addRejector(AgentSession agentSession) {
rejections.add(agentSession.getJID().toBareJID());
rejectionTimes.put(agentSession.getJID().toBareJID(), new Date());
}
public void removeRejector(AgentSession agentSession) {
rejections.remove(agentSession.getJID().toBareJID());
rejectionTimes.remove(agentSession.getJID().toBareJID());
}
public boolean isRejector(AgentSession agentSession) {
Date rejectionTime = rejectionTimes.get(agentSession.getJID().toBareJID());
boolean rejector = false;
if (rejectionTime != null) {
if (rejectionTime.getTime() > System.currentTimeMillis() - rejectionTimeout) {
rejector = true;
}
else {
rejectionTimes.remove(agentSession.getJID().toBareJID());
}
}
return rejector;
}
public List<AgentSession> getAcceptedSessions() {
return Collections.unmodifiableList(acceptedSessions);
}
public Collection<String> getRejections() {
return rejections;
}
public Date getOfferTime() {
return offerTime;
}
public long getTimeout() {
return timeout;
}
public void setTimeout(long timeout) {
this.timeout = timeout;
}
public void invite(AgentSession agentSession) {
if (acceptedSessions.contains(agentSession) && request != null) {
// 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.
request.offerAccepted(agentSession);
updateUserSession(ROUTED);
invitationSent = true;
}
}
public void waitForResolution() {
long timeoutTime = offerTime.getTime() + timeout;
while (timeoutTime > System.currentTimeMillis() && !isAccepted() && !pendingSessions.isEmpty()) {
try {
Thread.sleep(500); // half second polling
}
catch (InterruptedException e) {
// do nothing
}
}
if (!isAccepted()) {
try {
for (AgentSession session : pendingSessions) {
request.sendRevoke(session, queue);
reject(session);
}
}
catch (Exception e) {
// Ignore
}
}
}
public void cancel() {
cancelled = true;
// Handle when customer cancels.
if (!pendingSessions.isEmpty() || !acceptedSessions.isEmpty()) {
for (AgentSession session : pendingSessions) {
request.sendRevoke(session, queue);
}
for (AgentSession session : acceptedSessions) {
request.sendRevoke(session, queue);
}
pendingSessions.clear();
acceptedSessions.clear();
updateUserSession(USER_CANCELLED);
}
else {
updateUserSession(ROUTE_EXPIRED);
}
}
public void addPendingSession(AgentSession agentSession) {
pendingSessions.add(agentSession);
// reset the Offer time
offerTime = new Date();
}
/**
* Returns true if the offer is still outstanding. An offer is considered outstanding if it has
* not been cancelled and an invitation was not yet sent to an agent.
*
* @return true if the offer is still outstanding.
*/
public boolean isOutstanding() {
return !cancelled && !invitationSent;
}
@Override
public int hashCode() {
return request.hashCode();
}
@Override
public boolean equals(Object obj) {
boolean eq = false;
if (obj instanceof Offer){
Offer otherOffer = (Offer)obj;
eq = request.equals(otherOffer.getRequest());
}
return eq;
}
/**
* Updates the database tables with new session state, waitTime and metadata.
*
* @param state the state of this session.
*/
private void updateUserSession(int state) {
// Update the current session.
request.updateSession(state, offerTime.getTime());
}
}