/*
* $Id$
*
* Copyright 2006, The jCoderZ.org Project. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are
* met:
*
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above
* copyright notice, this list of conditions and the following
* disclaimer in the documentation and/or other materials
* provided with the distribution.
* * Neither the name of the jCoderZ.org Project nor the names of
* its contributors may be used to endorse or promote products
* derived from this software without specific prior written
* permission.
*
* THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS "AS IS" AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS AND CONTRIBUTORS
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
* BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
* WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
* OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
* ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package org.jcoderz.phoenix.servlet;
import java.awt.Color;
import java.io.DataInputStream;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.net.InetAddress;
import java.net.Socket;
import java.net.UnknownHostException;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Collections;
import java.util.ConcurrentModificationException;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Locale;
import java.util.Map;
import java.util.TimeZone;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.jcoderz.commons.util.IoUtil;
/**
* This servlet sends a simple ping to the given host and returns a
* 1 pixel image with a color representing the host status.
*
* @web.servlet name="ping"
* @web.servlet-mapping url-pattern="/ping/*"
*
* @author Andreas Mandel
*/
public final class PingServlet
extends HttpServlet
{
/**
* Timezone used by the protocol.
*/
public static final TimeZone TIME_ZONE = TimeZone.getTimeZone("UTC");
/** The format used to write date values. */
public static final String DATE_TIME_FORMAT
= "yyyy-MM-dd HH:mm:ss.SSS";
private static final int HOST_NOT_REACHED = -1;
private static final int UNKNOWN_HOST = -2;
private static final int ILLEGAL_ARGUMENT = -3;
private static final int HOST_REFUSED = -4;
private static final long serialVersionUID = 1L;
private static final int MAX_PING_CACHE_SIZE = 1000;
private static final Map PING_CACHE
= Collections.synchronizedMap(new HashMap());
private static long sLastCacheUpdate = 0;
private static boolean sUpdateInProgreess = false;
private static final long VALIDITY_TIME = 10 * 60 * 1000;
/**
* If a value in the cache in not used for this period of time it is
* removed from the cache.
*/
private static final long MAX_IDLE_TIME = 7 * 24 * 60 * 60 * 1000;
private static final int SOCKET_TIMEOUT = 1000;
private static final long FAST_RESPONSE = 5;
private static final int COLOR_OFFSET_RED = 13;
private static final int COLOR_OFFSET_GREEN = COLOR_OFFSET_RED + 1;
private static final int COLOR_OFFSET_BLUE = COLOR_OFFSET_GREEN + 1;
private static final int BG_OFFSET_RED = COLOR_OFFSET_BLUE + 1;
private static final int BG_OFFSET_GREEN = BG_OFFSET_RED + 1;
private static final int BG_OFFSET_BLUE = BG_OFFSET_GREEN + 1;
private static final byte [] IMAGE_DATA
= {
'G', 'I', 'F', '8', '7', 'a', //
1, 0, // logical width
1, 0, // logical height
(byte) 0x80, //
2, // BG color index
0, //
0, 0, 0, // COLOR 1 (used)
0, 0, 0, // COLOR 2 (unused)
44, // IMAGE
0, 0, // Left POS
0, 0, // Right POS
1, 0, // WIDTH
1, 0, // HEIGHT
0,
2, 2, 68, 01, 0, 59
};
private static final int ECHO_PORT = 22;
protected void doPost (HttpServletRequest req, HttpServletResponse rsp)
throws IOException
{
doGet(req, rsp);
}
protected void doGet (HttpServletRequest req, HttpServletResponse rsp)
throws IOException
{
String hostname = req.getParameter("host");
final String clear = req.getParameter("clear");
if (clear != null && clear.length() > 0)
{
PING_CACHE.clear();
}
if (hostname == null || hostname.length() == 0)
{
hostname = req.getPathInfo();
if (hostname != null && hostname.charAt(0) == '/')
{
hostname = hostname.substring(1);
}
}
if (hostname == null || hostname.length() == 0)
{
dumpCacheInfo(rsp);
}
else
{
sendAnswer(check(hostname), rsp);
}
updateCache();
}
private void dumpCacheInfo (HttpServletResponse rsp)
throws IOException
{
rsp.setContentType("text/plain");
final StringWriter sw = new StringWriter();
final PrintWriter data = new PrintWriter(sw);
synchronized (PING_CACHE)
{
data.println("Last Update: " + dateToString(sLastCacheUpdate));
data.println("Update in progress: " + sUpdateInProgreess);
data.println("Number of stored results: " + PING_CACHE.size());
data.println("Cached Data:");
data.println("---");
}
final PrintWriter out = rsp.getWriter();
out.print(sw.getBuffer().toString());
try
{
final Iterator i = PING_CACHE.values().iterator();
while (i.hasNext())
{
final PingResult result = (PingResult) i.next();
out.println(result);
}
out.println("---");
}
catch (ConcurrentModificationException ex)
{
out.println("Uups... somebody updated the cache... try again!");
}
}
private void updateCache ()
{
try
{
final long now = System.currentTimeMillis();
final long lastUpdate;
final boolean alreadyUpdating;
synchronized (PING_CACHE)
{
lastUpdate = sLastCacheUpdate;
alreadyUpdating = sUpdateInProgreess;
sUpdateInProgreess = true;
}
if (lastUpdate + VALIDITY_TIME < now && !alreadyUpdating)
{
final Iterator i = PING_CACHE.values().iterator();
while (i.hasNext())
{
final PingResult result = (PingResult) i.next();
if (result.getLastUsed() < (now - MAX_IDLE_TIME))
{
i.remove();
}
else if (result.getExpires() < now)
{
new Thread()
{
public void run ()
{
check(result.getHostname()); // update entry not list?
};
} .start();
}
}
synchronized (PING_CACHE)
{
sLastCacheUpdate = now;
}
}
}
catch (ConcurrentModificationException ex)
{
// we will catch up next time
}
finally
{
synchronized (PING_CACHE)
{
sUpdateInProgreess = false;
}
}
}
private void sendAnswer (PingResult result, HttpServletResponse rsp)
throws IOException
{
rsp.setContentType("image/gif");
rsp.setContentLength(IMAGE_DATA.length);
rsp.setHeader("Cache-Control", "public");
final byte[] responseData = new byte[IMAGE_DATA.length];
System.arraycopy(IMAGE_DATA, 0, responseData, 0, IMAGE_DATA.length);
if (result != null)
{
rsp.setDateHeader("Last-Modified", result.getLastModified());
rsp.setDateHeader("Expires", result.getExpires());
final Color color = resultToColor(result.getResult());
responseData[COLOR_OFFSET_RED] = (byte) color.getRed();
responseData[COLOR_OFFSET_GREEN] = (byte) color.getGreen();
responseData[COLOR_OFFSET_BLUE] = (byte) color.getBlue();
responseData[BG_OFFSET_RED] = (byte) color.getRed();
responseData[BG_OFFSET_GREEN] = (byte) color.getGreen();
responseData[BG_OFFSET_BLUE] = (byte) color.getBlue();
rsp.setHeader("response-value", String.valueOf(result.getResult()));
rsp.setHeader("response-host", result.getHostname());
}
rsp.getOutputStream().write(responseData);
}
private Color resultToColor (long result)
{
final Color responseColor;
if (result == UNKNOWN_HOST)
{
responseColor = Color.RED;
}
else if (result == ILLEGAL_ARGUMENT)
{
responseColor = Color.ORANGE;
}
else if (result == HOST_REFUSED)
{
responseColor = Color.YELLOW;
}
else if (result < 0)
{
responseColor = Color.BLACK;
}
else if (result <= FAST_RESPONSE)
{
responseColor = Color.GREEN.brighter();
}
else if (result <= (FAST_RESPONSE + FAST_RESPONSE))
{
responseColor = Color.GREEN;
}
else if (result <= (FAST_RESPONSE + FAST_RESPONSE + FAST_RESPONSE))
{
responseColor = Color.GREEN.darker();
}
else
{
responseColor = Color.GREEN.darker().darker();
}
return responseColor;
}
protected PingResult check (String host)
{
final long now = System.currentTimeMillis();
PingResult result = (PingResult) PING_CACHE.get(host);
if (result == null)
{
result = new PingResult(host, ping(host));
PING_CACHE.put(host, result);
}
else
{
// only one update per host
synchronized (result)
{
if (result.getExpires() < now)
{ // this might take some time!
result.setResult(ping(host));
}
}
}
// just to be save against memory attacks
if (PING_CACHE.size() > MAX_PING_CACHE_SIZE)
{
PING_CACHE.clear();
}
return result;
}
protected static long ping (String hostname)
{
final long result;
if (hostname == null || hostname.length() == 0)
{
result = ILLEGAL_ARGUMENT;
}
else
{
InetAddress host = null;
try
{
host = InetAddress.getByName(hostname);
}
catch (UnknownHostException ex)
{
// hmmm
}
if (host == null)
{
result = UNKNOWN_HOST;
}
else
{
result = ping(host);
}
}
return result;
}
protected static long ping (InetAddress host)
{
final long timer = System.currentTimeMillis();
DataInputStream dis = null;
Socket t = null;
long result = -1;
try
{
t = new Socket(host, ECHO_PORT);
t.setTcpNoDelay(true);
t.setSoTimeout(SOCKET_TIMEOUT);
dis = new DataInputStream(t.getInputStream());
dis.readByte();
result = System.currentTimeMillis() - timer;
}
catch (IOException e)
{
final String message = e.getMessage();
if (message != null && message.indexOf("refused") != -1)
{
result = HOST_REFUSED;
}
else
{
result = HOST_NOT_REACHED;
}
}
finally
{
IoUtil.close(dis);
IoUtil.close(t);
}
return result;
}
static String dateToString (long time)
{
final DateFormat formater
= new SimpleDateFormat(DATE_TIME_FORMAT, Locale.US);
formater.setTimeZone(TIME_ZONE);
return formater.format(new Date(time));
}
private static class PingResult
{
private final String mHostname;
private long mResult;
private long mExpires;
private long mLastModified;
private long mLastUsed;
PingResult (String hostname, long result)
{
final long now = System.currentTimeMillis();
mExpires = now + VALIDITY_TIME;
mLastModified = now;
mHostname = hostname;
mResult = result;
mLastUsed = now;
}
/** {@inheritDoc} */
public synchronized String toString ()
{
final StringBuffer buffer = new StringBuffer();
buffer.append("[PingResult: ");
buffer.append(mHostname);
buffer.append(" result: ");
buffer.append(mResult);
buffer.append(" expires: ");
buffer.append(dateToString(mExpires));
buffer.append(" lastModified: ");
buffer.append(dateToString(mLastModified));
buffer.append(" lastUsed: ");
buffer.append(dateToString(mLastUsed));
buffer.append(']');
return buffer.toString();
}
String getHostname ()
{
return mHostname;
}
synchronized long getExpires ()
{
return mExpires;
}
synchronized long getLastModified ()
{
return mLastModified;
}
synchronized void setResult (long result)
{
final long now = System.currentTimeMillis();
mExpires = now + VALIDITY_TIME;
if (result != mResult)
{
mLastModified = now;
mResult = result;
}
}
synchronized long getResult ()
{
mLastUsed = System.currentTimeMillis();
return mResult;
}
synchronized long getLastUsed ()
{
return mLastUsed;
}
}
}