org.jivesoftware.whack.ExternalComponent Maven / Gradle / Ivy
Show all versions of jamppa Show documentation
/**
* $RCSfile$
* $Revision: 10099 $
* $Date: 2008-03-20 16:46:17 -0700 (Thu, 20 Mar 2008) $
*
* Copyright 2005 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.whack;
import org.dom4j.DocumentException;
import org.dom4j.Element;
import org.dom4j.io.XMLWriter;
import org.dom4j.io.XPPPacketReader;
import org.jivesoftware.whack.util.StringUtils;
import org.jivesoftware.whack.util.TaskEngine;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
import org.xmlpull.v1.XmlPullParserFactory;
import org.xmpp.component.Component;
import org.xmpp.component.ComponentException;
import org.xmpp.component.ComponentManager;
import org.xmpp.packet.IQ;
import org.xmpp.packet.JID;
import org.xmpp.packet.Packet;
import org.xmpp.packet.StreamError;
import java.io.*;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.net.UnknownHostException;
import java.util.Iterator;
import java.util.Map;
import java.util.TimerTask;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
/**
* ExternalComponents are responsible for connecting and authenticating with a
* remote server and for sending and processing received packets. In fact, an
* ExternalComponent is a wrapper on a Component that provides remote connection
* capabilities. The actual processing of the packets is done by the wrapped
* Component.
*
* @author Gaston Dombiak
*/
public class ExternalComponent implements Component {
/**
* The utf-8 charset for decoding and encoding XMPP packet streams.
*/
private static String CHARSET = "UTF-8";
private Component component;
private ExternalComponentManager manager;
private Socket socket;
private XMLWriter xmlSerializer;
private XmlPullParserFactory factory = null;
private XPPPacketReader reader = null;
private Writer writer = null;
private boolean shutdown = false;
private boolean reconnecting = false;
private KeepAliveTask keepAliveTask;
private TimeoutTask timeoutTask;
/**
* Timestamp when the last stanza was sent to the server. This information
* is used by the keep alive process to only send heartbeats when the
* connection has been idle.
*/
private long lastActive = System.currentTimeMillis();
private String connectionID;
/**
* Hold the full domain of this component. The full domain is composed by
* the subdomain plus the domain of the server. E.g.
* conference.jivesoftware.com. The domain may change after a connection has
* been established with the server.
*/
private String domain;
/**
* Holds the subdomain that is associated to this component. The subdomain
* is the initial part of the domain. The subdomain cannot be affected after
* establishing a connection with the server. E.g. conference.
*/
private String subdomain;
/**
* Holds the IP address or host name where the connection must be made.
*/
private String host;
private int port;
/**
* Pool of threads that are available for processing the requests.
*/
private ThreadPoolExecutor threadPool;
/**
* Thread that will read the XML from the socket and ask this component to
* process the read packets.
*/
private SocketReadThread readerThread;
private Map resultListeners = new ConcurrentHashMap();
private Map resultTimeout = new ConcurrentHashMap();
public ExternalComponent(Component component,
ExternalComponentManager manager) {
// Be default create a pool of 25 threads to process the received
// requests
this(component, manager, 25);
}
public ExternalComponent(Component component,
ExternalComponentManager manager, int maxThreads) {
this.component = component;
this.manager = manager;
// Create a pool of threads that will process requests received by this
// component. If more
// threads are required then the command will be executed on the
// SocketReadThread process
threadPool = new ThreadPoolExecutor(maxThreads, maxThreads, 15,
TimeUnit.SECONDS, new LinkedBlockingQueue(),
new ThreadPoolExecutor.CallerRunsPolicy());
}
/**
* Generates a connection with the server and tries to authenticate. If an
* error occurs in any of the steps then a ComponentException is thrown.
*
* @param host
* the host to connect with.
* @param port
* the port to use.
* @param subdomain
* the subdomain that this component will be handling.
* @throws ComponentException
* if an error happens during the connection and authentication
* steps.
*/
public void connect(String host, int port, String subdomain)
throws ComponentException {
try {
// Open a socket to the server
this.socket = new Socket();
socket.connect(new InetSocketAddress(host, port),
manager.getConnectTimeout());
if (manager.getServerName() != null) {
this.domain = subdomain + "." + manager.getServerName();
} else {
this.domain = subdomain;
}
this.subdomain = subdomain;
// Keep these variables that will be used in case a reconnection is
// required
this.host = host;
this.port = port;
try {
factory = XmlPullParserFactory.newInstance();
reader = new XPPPacketReader();
reader.setXPPFactory(factory);
reader.getXPPParser()
.setInput(
new InputStreamReader(socket.getInputStream(),
CHARSET));
// Get a writer for sending the open stream tag
writer = new BufferedWriter(new OutputStreamWriter(
socket.getOutputStream(), CHARSET));
// Open the stream.
StringBuilder stream = new StringBuilder();
stream.append("");
writer.write(stream.toString());
writer.flush();
stream = null;
// Get the answer from the server
XmlPullParser xpp = reader.getXPPParser();
for (int eventType = xpp.getEventType(); eventType != XmlPullParser.START_TAG;) {
eventType = xpp.next();
}
// Set the streamID returned from the server
connectionID = xpp.getAttributeValue("", "id");
if (xpp.getAttributeValue("", "from") != null) {
this.domain = xpp.getAttributeValue("", "from");
}
xmlSerializer = new XMLWriter(writer);
// Handshake with the server
stream = new StringBuilder();
stream.append("");
stream.append(StringUtils.hash(connectionID
+ manager.getSecretKey(subdomain)));
stream.append(" ");
writer.write(stream.toString());
writer.flush();
stream = null;
// Get the answer from the server
try {
Element doc = reader.parseDocument().getRootElement();
if ("error".equals(doc.getName())) {
StreamError error = new StreamError(doc);
// Close the connection
socket.close();
socket = null;
// throw the exception with the wrapped error
throw new ComponentException(error);
}
// Everything went fine
// Start keep alive thread to send every 30 seconds of
// inactivity a heart beat
keepAliveTask = new KeepAliveTask();
TaskEngine.getInstance().scheduleAtFixedRate(keepAliveTask,
15000, 30000);
timeoutTask = new TimeoutTask();
TaskEngine.getInstance().scheduleAtFixedRate(timeoutTask,
2000, 2000);
} catch (DocumentException e) {
try {
socket.close();
} catch (IOException ioe) {
// Do nothing
}
throw new ComponentException(e);
} catch (XmlPullParserException e) {
try {
socket.close();
} catch (IOException ioe) {
// Do nothing
}
throw new ComponentException(e);
}
} catch (XmlPullParserException e) {
try {
socket.close();
} catch (IOException ioe) {
// Do nothing
}
throw new ComponentException(e);
}
} catch (UnknownHostException uhe) {
try {
if (socket != null)
socket.close();
} catch (IOException e) {
// Do nothing
}
throw new ComponentException(uhe);
} catch (IOException ioe) {
try {
if (socket != null)
socket.close();
} catch (IOException e) {
// Do nothing
}
throw new ComponentException(ioe);
}
}
public Component getComponent() {
return component;
}
public String getName() {
return component.getName();
}
public String getDescription() {
return component.getDescription();
}
/**
* Returns the domain provided by this component in the connected server.
* The domain is composed by the subdomain plus the domain of the server.
* E.g. conference.jivesoftware.com. The domain may change after a
* connection has been established with the server.
*
* @return the domain provided by this component in the connected server.
*/
public String getDomain() {
return domain;
}
/**
* Returns the subdomain provided by this component in the connected server.
* E.g. conference.
*
* @return the subdomain provided by this component in the connected server.
*/
public String getSubdomain() {
return subdomain;
}
/**
* Returns the ComponentManager that created this component.
*
* @return the ComponentManager that created this component.
*/
ExternalComponentManager getManager() {
return manager;
}
public void processPacket(final Packet packet) {
threadPool.execute(new Runnable() {
public void run() {
if (packet instanceof IQ) {
IQ iq = (IQ) packet;
IQ.Type iqType = iq.getType();
if (IQ.Type.result == iqType || IQ.Type.error == iqType) {
// The server got an answer to an IQ packet that was
// sent from the component
IQResultListener iqResultListener = resultListeners
.remove(iq.getID());
resultTimeout.remove(iq.getID());
if (iqResultListener != null) {
try {
iqResultListener.receivedAnswer(iq);
} catch (Exception e) {
manager.getLog()
.error("Error processing answer of remote entity",
e);
}
return;
}
}
}
component.processPacket(packet);
}
});
}
public void send(Packet packet) {
synchronized (writer) {
try {
xmlSerializer.write(packet.getElement());
xmlSerializer.flush();
// Keep track of the last time a stanza was sent to the server
lastActive = System.currentTimeMillis();
} catch (IOException e) {
// Log the exception
manager.getLog().error(e);
if (!shutdown) {
// Connection was lost so try to reconnect
connectionLost();
}
}
}
}
public void initialize(JID jid, ComponentManager componentManager)
throws ComponentException {
component.initialize(jid, componentManager);
}
public void start() {
// Everything went fine so start reading packets from the server
readerThread = new SocketReadThread(this, reader);
readerThread.setDaemon(true);
readerThread.start();
// Notify the component that it will be notified of new received packets
component.start();
}
public void shutdown() {
shutdown = true;
disconnect();
}
private void disconnect() {
if (readerThread != null) {
readerThread.shutdown();
}
threadPool.shutdown();
TaskEngine.getInstance().cancelScheduledTask(keepAliveTask);
TaskEngine.getInstance().cancelScheduledTask(timeoutTask);
if (socket != null && !socket.isClosed()) {
try {
synchronized (writer) {
try {
writer.write(" ");
xmlSerializer.flush();
} catch (IOException e) {
// Do nothing
}
}
} catch (Exception e) {
// Do nothing
}
try {
socket.close();
} catch (Exception e) {
manager.getLog().error(e);
}
}
}
/**
* Notification message that the connection with the server was lost
* unexpectedly. We will try to reestablish the connection for ever until
* the connection has been reestablished or this thread has been stopped.
*/
public void connectionLost() {
// Ensure that only one thread will try to reconnect.
synchronized (this) {
if (reconnecting) {
return;
}
reconnecting = true;
}
readerThread = null;
boolean isConnected = false;
if (!shutdown) {
// Notify the component that connection was lost so it needs to
// shutdown. The component is
// still registered in the local component manager but just not
// connected to the server
component.shutdown();
}
while (!isConnected && !shutdown) {
try {
connect(host, port, subdomain);
isConnected = true;
// It may be possible that while a new connection was being
// established the
// component was required to shutdown so in this case we need to
// close the new
// connection
if (shutdown) {
disconnect();
} else {
// Component is back again working so start it up again
start();
}
} catch (ComponentException e) {
manager.getLog().error(
"Error trying to reconnect with the server", e);
// Wait for 5 seconds until the next retry
try {
Thread.sleep(5000);
} catch (InterruptedException e1) {
// Do nothing
}
}
}
reconnecting = false;
}
/**
* Adds an {@link IQResultListener} that will be invoked when an IQ result
* is sent to the server itself and is of type result or error. This is a
* nice way for the server to send IQ packets to other XMPP entities and be
* waked up when a response is received back.
*
*
* Once an IQ result was received, the listener will be invoked and removed
* from the list of listeners.
*
* @param id
* the id of the IQ packet being sent from the server to an XMPP
* entity.
* @param listener
* the IQResultListener that will be invoked when an answer is
* received
* @param timeoutmillis
* The amount of milliseconds after which waiting for a response
* should be stopped.
*/
void addIQResultListener(String id, IQResultListener listener,
long timeoutmillis) {
// be generated by the server and simulate like the client sent it. This
// will let listeners
// react and be removed from the collection
resultListeners.put(id, listener);
resultTimeout.put(id, System.currentTimeMillis() + timeoutmillis);
}
/**
* A TimerTask that keeps connections to the server alive by sending a space
* character on an interval.
*/
private class KeepAliveTask extends TimerTask {
public void run() {
synchronized (writer) {
// Send heartbeat if no packet has been sent to the server for a
// given time
if (System.currentTimeMillis() - lastActive >= 30000) {
try {
writer.write(" ");
writer.flush();
} catch (IOException e) {
// Log the exception
manager.getLog().error(e);
if (!shutdown) {
// Connection was lost so try to reconnect
connectionLost();
}
} catch (Exception e) {
// Do nothing
}
}
}
}
}
/**
* Timer task that will remove Listeners that wait for results to IQ stanzas
* that have timed out. Time out values can be set to each listener
* individually by adjusting the timeout value in the third parameter of
* {@link ExternalComponent#addIQResultListener(String, IQResultListener, long)}
* .
*
* @author Guus der Kinderen, [email protected]
*/
private class TimeoutTask extends TimerTask {
/**
* Iterates over and removes all timed out results.
*
*
* The map that keeps track of timeout values is ordered by timeout
* date. This way, iteration can be stopped as soon as the first value
* has been found that didn't timeout yet.
*/
@Override
public void run() {
// Use an Iterator to allow changes to the Map that is backing
// the Iterator.
final Iterator> it = resultTimeout
.entrySet().iterator();
while (it.hasNext()) {
final Map.Entry pointer = it.next();
if (System.currentTimeMillis() < pointer.getValue()) {
// This entry has not expired yet. Ignore it.
continue;
}
final String packetId = pointer.getKey();
// remove this listener from the list
final IQResultListener listener = resultListeners
.remove(packetId);
if (listener != null) {
// notify listener of the timeout.
listener.answerTimeout(packetId);
}
// remove the packet from the list that's used to track
// timeouts
it.remove();
}
}
}
}