package net.jxta.impl.endpoint.servlethttp;
import static org.hamcrest.CoreMatchers.equalTo;
import static org.junit.Assert.assertThat;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.ConnectException;
import java.net.HttpURLConnection;
import java.net.InetAddress;
import java.net.ProtocolException;
import java.net.Socket;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.net.UnknownHostException;
import java.util.Arrays;
import java.util.Iterator;
import net.jxta.endpoint.EndpointAddress;
import net.jxta.endpoint.EndpointService;
import net.jxta.endpoint.Message;
import net.jxta.endpoint.MessengerEvent;
import net.jxta.endpoint.MessengerEventListener;
import net.jxta.endpoint.StringMessageElement;
import net.jxta.endpoint.WireFormatMessage;
import net.jxta.endpoint.WireFormatMessageFactory;
import net.jxta.exception.PeerGroupException;
import net.jxta.id.IDFactory;
import net.jxta.impl.util.threads.TaskManager;
import net.jxta.logging.Logger;
import net.jxta.logging.Logging;
import net.jxta.peer.PeerID;
import net.jxta.peergroup.PeerGroup;
import net.jxta.peergroup.PeerGroupID;
import net.jxta.test.util.JUnitRuleMockery;
import net.jxta.test.util.MessageUtil;
import org.apache.log4j.BasicConfigurator;
import org.apache.log4j.Level;
import org.jmock.Expectations;
import org.jmock.lib.concurrent.Synchroniser;
import org.junit.After;
import org.junit.AfterClass;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
/**
* Characterise the current behaviour of the Jetty servlet-based
* HttpMessageReceiver, to ensure nothing breaks when replacing Jetty 4.2.25
* with the latest version.
*
*/
public class CharacteriseHttpMessageReceiverTest {
private static final String CBJX_SYSTEM_PROPERTY = WireFormatMessageFactory.class.getName()+".CBJX_DISABLE";
private static Logger LOG;
private static final int port = 58000;
private static boolean initialCbjxDisable;
@Rule
public JUnitRuleMockery mockContext = new JUnitRuleMockery() {{
setThreadingPolicy(new Synchroniser());
}};
@Rule
public TemporaryFolder tempFolder = new TemporaryFolder();
private ServletHttpTransport mockServletHttpContext;
private EndpointService mockEndpointService;
private PeerGroup mockPeerGroup;
private StubMessengerEventListener stubMessengerEventListener = new StubMessengerEventListener();
private PeerGroupID assignedPeerGroupId;
private PeerID assignedPeerId;
private HttpMessageReceiver httpMessageReceiver;
private TaskManager taskManager;
@BeforeClass
public static void setupLogging() {
System.setProperty("net.jxta.logging.Logging", java.util.logging.Level.ALL.getName());
BasicConfigurator.configure();
LOG = Logging.getLogger(CharacteriseHttpMessageReceiverTest.class);
org.apache.log4j.Logger.getLogger(HttpMessageReceiver.class).setLevel(Level.ALL);
// I don't know what CBJX is, but it complicates message serialisation, so
// disable it.
initialCbjxDisable = Boolean.getBoolean(CBJX_SYSTEM_PROPERTY);
System.setProperty(CBJX_SYSTEM_PROPERTY, "true");
}
@AfterClass
public static void resetCbjx() {
System.setProperty(CBJX_SYSTEM_PROPERTY, "" + initialCbjxDisable);
}
private class StubMessengerEventListener implements MessengerEventListener, Runnable {
private Object lock = new Object();
private MessengerEvent event = null;
private Message replyMessage;
private volatile boolean startThread = false;
public boolean messengerReady(final MessengerEvent event) {
LOG.info("StubMessengerEventListener called with " + event);
// we need to stash this so that the test can get the messenger
synchronized(lock) {
this.event = event;
}
if (startThread) {
LOG.info("StubMessengerEventListener creating thread...");
new Thread(this).start();
return true; // to indicate that this messenger is 'taken'
}
return false;
}
public boolean wasListenerCalled() {
synchronized(lock) {
return event != null;
}
}
public void replyWith(final Message replyMessage) {
synchronized(lock) {
this.replyMessage = replyMessage;
startThread = (replyMessage != null);
}
}
public void run() {
LOG.info("StubMessengerEventListener reply thread starting");
try {
Thread.sleep(500);
LOG.info("StubMessengerEventListener out of sleep, locking...");
synchronized(lock) {
LOG.info("StubMessengerEventListener acquired lock, replying with " + replyMessage);
event.getMessenger().sendMessage(replyMessage);
LOG.info("StubMessengerEventListener replied");
}
} catch (final IOException e) {
LOG.warn("StubMessengerEventListener thread could not send", e);
} catch (final InterruptedException e) {
LOG.warn("StubMessengerEventListener thread interrupted", e);
}
LOG.info("StubMessengerEventListener reply thread finished");
}
}
@Before
public void setUp() throws PeerGroupException, UnknownHostException {
LOG.info("temp folder is " + tempFolder.getRoot().getAbsolutePath());
mockServletHttpContext = mockContext.mock(ServletHttpTransport.class);
mockEndpointService = mockContext.mock(EndpointService.class);
mockPeerGroup = mockContext.mock(PeerGroup.class);
assignedPeerGroupId = IDFactory.newPeerGroupID();
assignedPeerId = IDFactory.newPeerID(assignedPeerGroupId);
taskManager = new TaskManager();
// expectations of HttpMessageReceiver's constructor
mockContext.checking(new Expectations() {{
oneOf(mockServletHttpContext).getEndpointService(); will(returnValue(mockEndpointService));
oneOf(mockServletHttpContext).getAssignedID(); will(returnValue(assignedPeerGroupId));
oneOf(mockEndpointService).getGroup(); will(returnValue(mockPeerGroup));
oneOf(mockPeerGroup).getStoreHome(); will(returnValue(tempFolder.getRoot().toURI()));
}});
LOG.info("Creating HttpMessageReceiver");
assertThat(portOpen(), equalTo(false));
httpMessageReceiver = new HttpMessageReceiver(
mockServletHttpContext,
Arrays.asList(new EndpointAddress("urn:jxta:test#service/param")),
InetAddress.getLocalHost(), port,
Thread.currentThread().getContextClassLoader());
assertThat(portOpen(), equalTo(false));
// expectations of HttpMessageReceiver's start
mockContext.checking(new Expectations() {{
oneOf(mockServletHttpContext).getEndpointService(); will(returnValue(mockEndpointService));
oneOf(mockEndpointService).addMessageTransport(httpMessageReceiver); will(returnValue(stubMessengerEventListener));
}});
// Expectations of HttpMesssageServlet's init
mockContext.checking(new Expectations() {{
oneOf(mockServletHttpContext).getEndpointService(); will(returnValue(mockEndpointService));
oneOf(mockEndpointService).getGroup(); will(returnValue(mockPeerGroup));
oneOf(mockPeerGroup).getPeerID(); will(returnValue(assignedPeerId));
}});
LOG.info("Starting HttpMessageReceiver");
httpMessageReceiver.start();
assertThat(portOpen(), equalTo(true));
}
@After
public void tearDown() {
assertThat(portOpen(), equalTo(true));
LOG.info("Stopping HttpMessageReceiver");
// expectations of HttpMessageReceiver's stop
mockContext.checking(new Expectations() {{
oneOf(mockServletHttpContext).getEndpointService(); will(returnValue(mockEndpointService));
oneOf(mockEndpointService).removeMessageTransport(httpMessageReceiver); // ?
}});
httpMessageReceiver.stop();
assertThat(portOpen(), equalTo(false));
taskManager.shutdown();
}
private interface HttpConnectionTestBody {
void apply(HttpURLConnection httpConnection) throws IOException, URISyntaxException, InterruptedException;
}
@Test(timeout = 3000)
public void httpMessageServletCanBePinged() throws PeerGroupException, IOException, URISyntaxException, InterruptedException {
// No Message and no Requestor defined.
connect("", new HttpConnectionTestBody() {
public void apply(HttpURLConnection httpConnection) throws IOException, URISyntaxException {
// not sure if this is the recommended way of going from the
// string received in the input stream, which is of the form
// cbid-2FD774D5B372433D84F1BAF6098F73C05BBC13E206EB725BAD1AD059E49FC57F03
// to a PeerID
final String readFromStream = readFromStream(httpConnection.getInputStream(), httpConnection.getContentLength());
final URI uri = new URI("urn:jxta:" + readFromStream);
final PeerID receivedPeerId = (PeerID) IDFactory.fromURI(uri);
LOG.info("Received peer Id '" + receivedPeerId + "'");
assertThat(receivedPeerId, equalTo(assignedPeerId));
}
});
}
@Test(timeout = 3000)
public void httpMessageServletCanBePolledWithEmptyMessageAndNoReplyMessenger() throws PeerGroupException, IOException, URISyntaxException, InterruptedException {
// Requestor defined, positive response timeout and destination address.
final PeerID requestorPeerId = IDFactory.newPeerID(assignedPeerGroupId);
final EndpointAddress destinationAddress = new EndpointAddress("jxta://test/service/param");
mockContext.checking(new Expectations() {{
oneOf(mockServletHttpContext).getPeerGroup(); will(returnValue(mockPeerGroup));
oneOf(mockPeerGroup).getPeerGroupID(); will(returnValue(assignedPeerGroupId));
oneOf(mockPeerGroup).getTaskManager(); will(returnValue(taskManager));
}});
connect(requestorPeerId.toString() + "?500,600," + destinationAddress,
new HttpConnectionTestBody() {
public void apply(final HttpURLConnection httpConnection) throws IOException, URISyntaxException {
final String readFromStream = readFromStream(httpConnection.getInputStream(), httpConnection.getContentLength());
assertThat(readFromStream, equalTo(""));
assertThat(httpConnection.getResponseCode(), equalTo(HttpURLConnection.HTTP_OK));
// And the above expectations show that the endpoint service
// has not been given a message (since there is no message
// content to our message).
}
});
}
@Test(timeout = 3000)
public void httpMessageServletCanBePolledWithMessageThatsDeliveredToEndpointService() throws PeerGroupException, IOException, URISyntaxException, InterruptedException {
// Requestor defined, positive response timeout and destination address.
final PeerID requestorPeerId = IDFactory.newPeerID(assignedPeerGroupId);
final EndpointAddress destinationAddress = new EndpointAddress("jxta://test/service/param");
// Going to send a message that'll be serialised, sent over HTTP, received,
// deserialised, and then passed to the endpoint service.
final Message message = new Message();
message.addMessageElement(new StringMessageElement("myname", "mymessage", null));
mockContext.checking(new Expectations() {{
atLeast(2).of(mockServletHttpContext).getPeerGroup(); will(returnValue(mockPeerGroup));
oneOf(mockPeerGroup).getPeerGroupID(); will(returnValue(assignedPeerGroupId));
oneOf(mockPeerGroup).getTaskManager(); will(returnValue(taskManager));
// The endpoint service is given the deserialised message.
oneOf(mockEndpointService).processIncomingMessage(with(equalTo(message)));
}});
connect(requestorPeerId.toString() + "?500,600," + destinationAddress,
// Prepare the request....
new HttpConnectionTestBody() {
public void apply(HttpURLConnection httpConnection) throws IOException, URISyntaxException {
final WireFormatMessage wireExternal = WireFormatMessageFactory.toWireExternal(message, WireFormatMessageFactory.DEFAULT_WIRE_MIME, null, null);
httpConnection.addRequestProperty("content-length", "" + wireExternal.getByteLength());
httpConnection.addRequestProperty("content-type", WireFormatMessageFactory.DEFAULT_WIRE_MIME.getMimeMediaType());
httpConnection.setDoOutput(true);
wireExternal.sendToStream(httpConnection.getOutputStream());
}
},
// Check the response....
new HttpConnectionTestBody() {
public void apply(final HttpURLConnection httpConnection) throws IOException, URISyntaxException {
final String readFromStream = readFromStream(httpConnection.getInputStream(), httpConnection.getContentLength());
assertThat(readFromStream, equalTo(""));
assertThat(httpConnection.getResponseCode(), equalTo(HttpURLConnection.HTTP_OK));
}
});
}
@Test(timeout = 8000)
public void httpMessageServletCanBePolledWithReply() throws PeerGroupException, IOException, URISyntaxException, InterruptedException {
// Requestor defined, positive response timeout and destination address.
final PeerID requestorPeerId = IDFactory.newPeerID(assignedPeerGroupId);
final EndpointAddress destinationAddress = new EndpointAddress("jxta://test/service/param");
mockContext.checking(new Expectations() {{
atLeast(2).of(mockServletHttpContext).getPeerGroup(); will(returnValue(mockPeerGroup));
oneOf(mockPeerGroup).getPeerGroupID(); will(returnValue(assignedPeerGroupId));
oneOf(mockPeerGroup).getTaskManager(); will(returnValue(taskManager));
}});
// The StubMessengerEventListener will be called, and claim the messenger as taken
// then we can get the messenger that was created by HttpMessageServer::processRequest
// (around line 290). We want to reply with this message...
final Message replyMessage = new Message();
replyMessage.addMessageElement(new StringMessageElement("anothername", "replymessage", null));
dumpMessage("reply", replyMessage);
stubMessengerEventListener.replyWith(replyMessage);
connect(requestorPeerId.toString() + "?4000,1000," + destinationAddress,
new HttpConnectionTestBody() {
public void apply(final HttpURLConnection httpConnection) throws IOException, URISyntaxException, InterruptedException {
LOG.debug("Reading from http connection input stream in 3s... ");
Thread.sleep(3000);
final Message incomingMessage = WireFormatMessageFactory.fromWireExternal(httpConnection.getInputStream(), WireFormatMessageFactory.DEFAULT_WIRE_MIME, WireFormatMessageFactory.DEFAULT_WIRE_MIME, null);
dumpMessage("incoming reply", incomingMessage);
assertThat(httpConnection.getResponseCode(), equalTo(HttpURLConnection.HTTP_OK));
assertThat(incomingMessage, equalTo(replyMessage));
assertThat(stubMessengerEventListener.wasListenerCalled(), equalTo(true));
}
});
}
@Test(timeout = 8000)
public void httpMessageServletCanBePolledWithReplyContentLengthSet() throws PeerGroupException, IOException, URISyntaxException, InterruptedException {
// Requestor defined, positive response timeout and destination address.
final PeerID requestorPeerId = IDFactory.newPeerID(assignedPeerGroupId);
final EndpointAddress destinationAddress = new EndpointAddress("jxta://test/service/param");
mockContext.checking(new Expectations() {{
atLeast(2).of(mockServletHttpContext).getPeerGroup(); will(returnValue(mockPeerGroup));
oneOf(mockPeerGroup).getPeerGroupID(); will(returnValue(assignedPeerGroupId));
oneOf(mockPeerGroup).getTaskManager(); will(returnValue(taskManager));
}});
// The StubMessengerEventListener will be called, and claim the messenger as taken
// then we can get the messenger that was created by HttpMessageServer::processRequest
// (around line 290). We want to reply with this message...
final Message replyMessage = new Message();
replyMessage.addMessageElement(new StringMessageElement("anothername", "replymessage", null));
dumpMessage("reply", replyMessage);
stubMessengerEventListener.replyWith(replyMessage);
// Using a negative extra responses timeout causes the content length
// to be set in the response, rather than using chunked encoding.
connect(requestorPeerId.toString() + "?4000,-1000," + destinationAddress,
new HttpConnectionTestBody() {
public void apply(final HttpURLConnection httpConnection) throws IOException, URISyntaxException, InterruptedException {
LOG.debug("Reading from http connection input stream in 3s... ");
Thread.sleep(3000);
final Message incomingMessage = WireFormatMessageFactory.fromWireExternal(httpConnection.getInputStream(), WireFormatMessageFactory.DEFAULT_WIRE_MIME, WireFormatMessageFactory.DEFAULT_WIRE_MIME, null);
dumpMessage("incoming reply", incomingMessage);
assertThat(httpConnection.getResponseCode(), equalTo(HttpURLConnection.HTTP_OK));
assertThat(incomingMessage, equalTo(replyMessage));
assertThat(stubMessengerEventListener.wasListenerCalled(), equalTo(true));
}
});
}
@Test(timeout = 3000)
public void httpMessageServletCanBeSentToWithDestinationAddress() throws PeerGroupException, IOException, URISyntaxException, InterruptedException {
// Requestor defined, positive response timeout or no destination address.
final PeerID requestorPeerId = IDFactory.newPeerID(assignedPeerGroupId);
// Going to send a message that'll be serialised, sent over HTTP, received,
// deserialised, and then passed to the endpoint service.
final Message message = new Message();
message.addMessageElement(new StringMessageElement("myname", "mymessage", null));
mockContext.checking(new Expectations() {{
oneOf(mockServletHttpContext).getPeerGroup(); will(returnValue(mockPeerGroup));
// The endpoint service is given the deserialised message.
oneOf(mockEndpointService).processIncomingMessage(with(equalTo(message)));
}});
connect(requestorPeerId.toString() + "?500", // no destination => no messenger
// Prepare the request....
new HttpConnectionTestBody() {
public void apply(HttpURLConnection httpConnection) throws IOException, URISyntaxException {
final WireFormatMessage wireExternal = WireFormatMessageFactory.toWireExternal(message, WireFormatMessageFactory.DEFAULT_WIRE_MIME, null, null);
httpConnection.addRequestProperty("content-length", "" + wireExternal.getByteLength());
httpConnection.addRequestProperty("content-type", WireFormatMessageFactory.DEFAULT_WIRE_MIME.getMimeMediaType());
httpConnection.setDoOutput(true);
wireExternal.sendToStream(httpConnection.getOutputStream());
}
},
// Check the response....
new HttpConnectionTestBody() {
public void apply(final HttpURLConnection httpConnection) throws IOException, URISyntaxException {
final String readFromStream = readFromStream(httpConnection.getInputStream(), httpConnection.getContentLength());
assertThat(readFromStream, equalTo(""));
assertThat(httpConnection.getResponseCode(), equalTo(HttpURLConnection.HTTP_OK));
// If there's no destination, the messenger is never created,
// and the listener shouldn't be called.
assertThat(stubMessengerEventListener.wasListenerCalled(), equalTo(false));
}
});
}
// TODO Need tests for the support for downloading midlets.
private void dumpMessage(final String description, final Message message) {
LOG.info(">> The " + description + " message is...");
final Iterator<String> msgit = MessageUtil.messageStatsIterator(message, true);
while (msgit.hasNext()) {
LOG.info("|| " + msgit.next());
}
LOG.info("<< The " + description + " message was.");
}
private void connect(final String restOfURL, final HttpConnectionTestBody body) throws IOException,
ProtocolException, URISyntaxException, InterruptedException {
connect(restOfURL, null, body);
}
private void connect(
final String restOfURL,
final HttpConnectionTestBody prepare,
final HttpConnectionTestBody check) throws IOException,
ProtocolException, URISyntaxException, InterruptedException {
// TODO if connected to a real network, getLocalHost gives real IP address. If
// trying to use localhost here, connection fails... how can I force JXTA to
// only use localhost (that should be sufficient for these tests?)
final URL url = new URL("http://" + InetAddress.getLocalHost().getHostAddress() + ":" + port + "/" + restOfURL);
final HttpURLConnection httpConnection = (HttpURLConnection) url.openConnection();
try {
httpConnection.setRequestMethod("GET");
httpConnection.setConnectTimeout(500);
httpConnection.setReadTimeout(1000);
if (prepare != null) {
LOG.info("Preparing connection...");
prepare.apply(httpConnection);
}
LOG.info("Connecting to " + url);
httpConnection.connect();
LOG.info("Connected");
check.apply(httpConnection);
} finally {
LOG.info("Disconnecting");
httpConnection.disconnect();
}
}
private String readFromStream(final InputStream inputStream, final int len)
throws IOException {
BufferedReader reader = null;
try {
char buf[] = new char[len];
reader = new BufferedReader(new InputStreamReader(inputStream));
return new String(buf, 0, reader.read(buf, 0, len));
} finally {
try {
if (reader != null) {
reader.close();
}
} catch (IOException ioe) {
LOG.warn("Could not close HttpURLConnection", ioe);
}
}
}
private Boolean portOpen() {
Socket socket = null;
try {
socket = new Socket(InetAddress.getLocalHost(), port);
final boolean connected = socket.isConnected();
LOG.info(connected ? "Connected to port " + port : "Could not connect to port " + port);
return connected;
} catch (ConnectException ce) {
LOG.info("Connection refused");
return false;
} catch (IOException ioe) {
LOG.warn("IOException while setting up socket", ioe);
} finally {
if (socket != null) {
try {
LOG.info("Closing connection");
socket.close();
} catch (IOException ioe) {
LOG.warn("IOException on close", ioe);
}
}
}
return false;
}
}