/*
* Tigase Jabber/XMPP Server
* Copyright (C) 2004-2012 "Artur Hefczyc" <artur.hefczyc@tigase.org>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. Look for COPYING file in the top folder.
* If not, see http://www.gnu.org/licenses/.
*
* $Rev$
* Last modified by $Author$
* $Date$
*/
package tigase.xmpp;
//~--- non-JDK imports --------------------------------------------------------
import tigase.net.IOService;
import tigase.net.IOServiceListener;
import tigase.server.Packet;
import tigase.util.TigaseStringprepException;
import tigase.xml.Element;
import tigase.xml.SimpleParser;
import tigase.xml.SingletonFactory;
//~--- JDK imports ------------------------------------------------------------
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
import java.util.Map;
import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.ConcurrentSkipListMap;
import java.util.concurrent.locks.ReentrantLock;
import java.util.logging.Level;
import java.util.logging.Logger;
//~--- classes ----------------------------------------------------------------
/**
* Describe class XMPPIOService here.
*
*
* Created: Tue Feb 7 07:15:02 2006
*
* @param <RefObject>
* is a refrence object stored by this service. This is e reference to
* higher level data object keeping more information about the
* connection.
* @author <a href="mailto:artur.hefczyc@tigase.org">Artur Hefczyc</a>
* @version $Rev$
*/
public class XMPPIOService<RefObject> extends IOService<RefObject> {
/**
* Variable <code>log</code> is a class logger.
*/
private static final Logger log = Logger.getLogger(XMPPIOService.class.getName());
public static final String CROSS_DOMAIN_POLICY_FILE_PROP_KEY =
"cross-domain-policy-file";
public static final String CROSS_DOMAIN_POLICY_FILE_PROP_VAL =
"etc/cross-domain-policy.xml";
public static final String REQ_NAME = "req";
public static final String ID_ATT = "id";
public static final String ACK_NAME = "ack";
private XMPPDomBuilderHandler<RefObject> domHandler = null;
protected SimpleParser parser = SingletonFactory.getParserInstance();
@SuppressWarnings("rawtypes")
private XMPPIOServiceListener serviceListener = null;
private static String cross_domain_policy = null;
/**
* The <code>waitingPackets</code> queue keeps data which have to be
* processed.
*/
private ConcurrentLinkedQueue<Packet> waitingPackets =
new ConcurrentLinkedQueue<Packet>();
private ConcurrentSkipListMap<String, Packet> waitingForAck =
new ConcurrentSkipListMap<String, Packet>();
private boolean white_char_ack = false;
private boolean xmpp_ack = false;
private boolean strict_ack = false;
private long req_idx = 0;
/**
* The <code>readyPackets</code> queue keeps data which have been already
* processed and they are actual processing results.
*/
private ConcurrentLinkedQueue<Packet> receivedPackets =
new ConcurrentLinkedQueue<Packet>();
private String xmlns = null;
private boolean firstPacket = true;
/** Field description */
public ReentrantLock writeInProgress = new ReentrantLock();
// ~--- constructors ---------------------------------------------------------
// /**
// * Variable <code>lock</code> keeps reference to object lock.
// * It supports multi-threaded processing and can be called simultaneously
// from
// * many threads. It is not recommended however as lock prevents most of
// * methods to be executed concurrently as they process data received from
// * socket and the data should be processed in proper order.
// */
// private Lock writeLock = new ReentrantLock();
// private Lock readLock = new ReentrantLock();
// private boolean streamClosed = false;
/**
* Creates a new <code>XMPPIOService</code> instance.
*
*/
public XMPPIOService() {
super();
domHandler = new XMPPDomBuilderHandler<RefObject>(this);
if (cross_domain_policy == null) {
String file_name =
System.getProperty(CROSS_DOMAIN_POLICY_FILE_PROP_KEY,
CROSS_DOMAIN_POLICY_FILE_PROP_VAL);
try {
BufferedReader br = new BufferedReader(new FileReader(file_name));
String line = br.readLine();
StringBuilder sb = new StringBuilder();
while (line != null) {
sb.append(line);
line = br.readLine();
}
sb.append('\0');
br.close();
cross_domain_policy = sb.toString();
} catch (Exception ex) {
log.log(Level.WARNING, "Problem reading cross domain poicy file: " + file_name,
ex);
}
}
}
public void setAckMode(boolean white_char_ack, boolean xmpp_ack, boolean strict) {
this.white_char_ack = white_char_ack;
this.xmpp_ack = xmpp_ack;
this.strict_ack = strict;
}
// ~--- methods --------------------------------------------------------------
/**
* Method <code>addPacketToSend</code> adds new data which will be processed
* during next run. Data are kept in proper order like in <em>FIFO</em> queue.
*
* @param packet
* a <code>Packet</code> value of data to process.
*/
public void addPacketToSend(Packet packet) {
if (xmpp_ack) {
String req = "" + (++req_idx);
packet.getElement().addChild(new Element(REQ_NAME,
new String[] {ID_ATT}, new String[] {req}));
waitingForAck.put(req, packet);
}
waitingPackets.offer(packet);
}
// ~--- get methods ----------------------------------------------------------
/**
* Method description
*
*
* @return
*/
public Queue<Packet> getReceivedPackets() {
return receivedPackets;
}
/**
* Method description
*
*
* @return
*/
public String getXMLNS() {
return this.xmlns;
}
// ~--- methods --------------------------------------------------------------
/**
* Describe <code>processWaitingPackets</code> method here.
*
* @throws IOException
*/
@Override
public void processWaitingPackets() throws IOException {
Packet packet = null;
// int cnt = 0;
// while ((packet = waitingPackets.poll()) != null && (cnt < 1000)) {
while ((packet = waitingPackets.poll()) != null) {
// ++cnt;
if (log.isLoggable(Level.FINEST)) {
log.log(Level.FINEST, "{0}, Sending packet: {1}", new Object[] { toString(),
packet });
}
writeRawData(packet.getElement().toString());
if (log.isLoggable(Level.FINEST)) {
log.log(Level.FINEST, "{0}, SENT: {1}", new Object[] { toString(),
packet.getElement().toString() });
}
} // end of while (packet = waitingPackets.poll() != null)
}
// ~--- set methods ----------------------------------------------------------
/**
* Method description
*
*
* @param servList
*/
@SuppressWarnings({ "unchecked", "rawtypes" })
public void setIOServiceListener(XMPPIOServiceListener servList) {
this.serviceListener = servList;
super.setIOServiceListener(servList);
}
/**
* Method description
*
*
* @param xmlns
*/
public void setXMLNS(String xmlns) {
this.xmlns = xmlns;
}
// ~--- methods --------------------------------------------------------------
/**
* Describe <code>stop</code> method here.
*
*/
@Override
public void stop() {
// if (!streamClosed) {
// streamClosed = true;
// serviceListener.xmppStreamClosed(this);
// } // end of if (!streamClosed)
super.stop();
}
/**
* Method description
*
*
* @param data
*
* @throws IOException
*/
public void writeRawData(String data) throws IOException {
// We change state of this object in this method
// It can be called by many threads simultanously
// so we need to make it thread-safe
// writeLock.lock();
// try {
writeData(data);
// } finally {
// writeLock.unlock();
// }
}
/**
* Method description
*
*
* @param data
*/
public void xmppStreamOpen(final String data) {
try {
if (log.isLoggable(Level.FINEST)) {
log.log(Level.FINEST, "{0}, Sending data: {1}", new Object[] { toString(), data });
}
writeRawData(data);
assert debug(data, "--SENT:");
} catch (IOException e) {
log.log(Level.WARNING, "{0}, Error sending stream open data: {1}", new Object[] {
toString(), e });
forceStop();
}
}
/**
* Method <code>addReceivedPacket</code> puts processing results to queue. The
* processing results are usually data (messages) which has been just received
* from socket.
*
* @param packet
* a <code>Packet</code> value of processing results.
*/
protected void addReceivedPacket(final Packet packet) {
if (firstPacket) {
if ("policy-file-request" == packet.getElemName()) {
log.fine("Got flash cross-domain request" + packet);
if (cross_domain_policy != null) {
try {
writeRawData(cross_domain_policy);
} catch (Exception ex) {
log.log(Level.INFO, "Can't send cross-domain policy: ", ex);
}
log.log(Level.FINER, "Cross-domain policy sent: {1}", cross_domain_policy);
} else {
log.log(Level.FINER, "No cross-domain policy defined to sent.");
}
return;
}
firstPacket = false;
}
if (packet.getElemName() == ACK_NAME) {
} else {
sendAck(packet);
receivedPackets.offer(packet);
}
}
/**
* Describe <code>processSocketData</code> method here.
*
* @exception IOException
* if an error occurs
*/
@Override
protected void processSocketData() throws IOException {
// We change state of this object in this method
// It can be called by many threads simultanously
// so we need to make it thread-safe
// log.finer("About to read socket data.");
// Correction:
// The design is that this method should not be called concurrently by
// multiple threads. However it may happen in some specific cases.
// There is a 'non-blocking' synchronization in IOService.call() method
// implemented instead.
// readLock.lock();
// try {
if (isConnected()) {
char[] data = readData();
while (isConnected() && (data != null) && (data.length > 0)) {
if (log.isLoggable(Level.FINEST)) {
log.log(Level.FINEST, "{0}, READ:\n{1}", new Object[] { toString(),
new String(data) });
}
boolean disconnect = checkData(data);
if (disconnect) {
if (log.isLoggable(Level.FINE)) {
log.log(Level.FINE, "{0}, checkData says disconnect: {1}", new Object[] {
toString(), new String(data) });
} else {
log.log(Level.WARNING, "{0}, checkData says disconnect",
toString());
}
forceStop();
return;
// domHandler = new XMPPDomBuilderHandler<RefObject>(this);
}
// This is log for debugging only,
// in normal mode don't even call below code
assert debug(new String(data), "--RECEIVED:");
Element elem = null;
try {
parser.parse(domHandler, data, 0, data.length);
if (domHandler.parseError()) {
if (log.isLoggable(Level.FINE)) {
log.log(Level.FINE, "{0}, Data parsing error: {1}", new Object[] {
toString(), new String(data) });
} else {
log.log(Level.WARNING, "{0}, data parsing error, stopping connection",
toString());
}
forceStop();
return;
// domHandler = new XMPPDomBuilderHandler<RefObject>(this);
}
Queue<Element> elems = domHandler.getParsedElements();
if (elems.size() > 0) {
readCompleted();
}
while ((elem = elems.poll()) != null) {
// assert debug(elem.toString() + "\n");
// log.finer("Read element: " + elem.getName());
if (log.isLoggable(Level.FINEST)) {
log.log(Level.FINEST, "{0}, Read packet: {1}", new Object[] { toString(),
elem });
}
// System.out.print(elem.toString());
Packet pack = Packet.packetInstance(elem);
addReceivedPacket(pack);
sendAck(pack);
} // end of while ((elem = elems.poll()) != null)
} catch (TigaseStringprepException ex) {
log.log(Level.INFO, toString() + ", Incorrect to/from JID format for stanza: "
+ elem.toString(), ex);
} catch (Exception ex) {
log.log(Level.INFO, toString() + ", Incorrect XML data: " + new String(data)
+ ", stopping connection: " + getConnectionId() + ", exception: ", ex);
forceStop();
} // end of try-catch
data = readData();
}
} else {
if (log.isLoggable(Level.FINE)) {
log.log(Level.FINE,
"{0}, function called when the service is not connected! forceStop()",
toString());
}
forceStop();
}
// } finally {
// readLock.unlock();
// }
}
private void sendAck(Packet packet) {
// If stanza receiving confirmation is configured, try to send confirmation
// back
if (white_char_ack || xmpp_ack) {
String ack = null;
if (white_char_ack) {
// If confirming via white space is enabled then prepare space ack.
ack = " ";
}
if (xmpp_ack) {
// If confirmation using XMPP is enabled prepare XMPP ack which may
// overwrite white space ACK.
// TODO: Write documentation about the stuff implemented here.
// If the stanza contains 'req' attribute, the server sends 'ack'
// response.
// Storing ack/req information in attribute might be more efficient, faster and less resources
// consuming, however it may also break the spec.
String req_val = packet.getAttribute(REQ_NAME);
// If the req is not in an attribute, let's check the extra payload
if (req_val == null) {
req_val = packet.getElement().getChildAttribute(REQ_NAME, ID_ATT);
}
if (req_val != null) {
// XMPP ack might be enabled in configuration but the client may not
// support it. In such a case we do not send XMPP ack.
ack = "<" + ACK_NAME + " " + ID_ATT + "=\"" + req_val + "\"/>";
}
}
if (ack != null) {
try {
writeRawData(ack);
log.log(Level.FINEST, "Sent ack confirmation: '" + ack + "'");
} catch (Exception ex) {
forceStop();
log.log(Level.FINE, "Can't send ack confirmation: '" + ack + "'", ex);
}
}
}
}
/**
* @param dat
* @return
*/
public boolean checkData(char[] data) throws IOException {
// by default do nothing and return false
return false;
}
@Override
protected int receivedPackets() {
return receivedPackets.size();
}
@SuppressWarnings({ "unchecked" })
protected void xmppStreamClosed() {
if (log.isLoggable(Level.FINEST)) {
log.log(Level.FINEST, "{0}, Received STREAM-CLOSE from the client", toString());
}
try {
if (log.isLoggable(Level.FINEST)) {
log.log(Level.FINEST, "{0}, Sending data: </stream:stream>", toString());
}
writeRawData("</stream:stream>");
} catch (IOException e) {
log.log(Level.INFO, "{0}, Error sending stream closed data: {1}", new Object[] {
toString(), e });
}
// streamClosed = true;
if (serviceListener != null) {
serviceListener.xmppStreamClosed(this);
}
// try {
// stop();
// } catch (IOException e) {
// log.warning("Error stopping service: " + e);
// } // end of try-catch
}
@SuppressWarnings({ "unchecked" })
protected void xmppStreamOpened(Map<String, String> attribs) {
if (serviceListener != null) {
String response = serviceListener.xmppStreamOpened(this, attribs);
try {
if (log.isLoggable(Level.FINEST)) {
log.log(Level.FINEST, "{0}, Sending data: {1}", new Object[] { toString(),
response });
}
writeRawData(response);
processWaitingPackets();
if ((response != null) && response.endsWith("</stream:stream>")) {
stop();
} // end of if (response.endsWith())
} catch (IOException e) {
log.log(Level.WARNING, "{0}, Error sending stream open data: {1}", new Object[] {
toString(), e });
forceStop();
}
}
}
} // XMPPIOService
// ~ Formatted in Sun Code Convention
// ~ Formatted by Jindent --- http://www.jindent.com