summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorSimon Rettberg2015-05-21 16:39:25 +0200
committerSimon Rettberg2015-05-21 16:39:25 +0200
commit45ada5475192f6afc9645c401de46765be87ee3f (patch)
treee5fa80f72f85995a95c2a80282a03e48b32a6c27
downloaddnbd3-status-45ada5475192f6afc9645c401de46765be87ee3f.tar.gz
dnbd3-status-45ada5475192f6afc9645c401de46765be87ee3f.tar.xz
dnbd3-status-45ada5475192f6afc9645c401de46765be87ee3f.zip
Lean and mean first prototype - to be improved!
-rw-r--r--.gitignore9
-rw-r--r--pom.xml69
-rw-r--r--src/main/java/fi/iki/elonen/NanoHTTPD.java1098
-rw-r--r--src/main/java/org/openslx/dnbd3/status/App.java14
-rw-r--r--src/main/java/org/openslx/dnbd3/status/StatisticsGenerator.java146
-rw-r--r--src/main/java/org/openslx/dnbd3/status/WebServer.java100
-rw-r--r--src/main/java/org/openslx/dnbd3/status/output/EdgeSerializer.java25
-rw-r--r--src/main/java/org/openslx/dnbd3/status/output/OutputMain.java19
-rw-r--r--src/main/java/org/openslx/dnbd3/status/output/ServerStats.java19
-rw-r--r--src/main/java/org/openslx/dnbd3/status/poller/ServerPoller.java70
-rw-r--r--src/main/java/org/openslx/dnbd3/status/rpc/Client.java31
-rw-r--r--src/main/java/org/openslx/dnbd3/status/rpc/Image.java37
-rw-r--r--src/main/java/org/openslx/dnbd3/status/rpc/Status.java73
-rw-r--r--src/main/java/org/openslx/graph/ClientNode.java24
-rw-r--r--src/main/java/org/openslx/graph/Edge.java128
-rw-r--r--src/main/java/org/openslx/graph/Graph.java559
-rw-r--r--src/main/java/org/openslx/graph/Node.java198
-rw-r--r--src/main/java/org/openslx/graph/PngRenderer.java46
-rw-r--r--src/main/java/org/openslx/graph/ServerNode.java23
-rw-r--r--static/index.html7
20 files changed, 2695 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..4beb56d
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,9 @@
+.project
+.settings/
+.classpath
+*.swp
+*~
+*.tmp
+*.class
+target/
+
diff --git a/pom.xml b/pom.xml
new file mode 100644
index 0000000..1a3a3e9
--- /dev/null
+++ b/pom.xml
@@ -0,0 +1,69 @@
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+ <modelVersion>4.0.0</modelVersion>
+ <groupId>org.openslx</groupId>
+ <artifactId>dnbd3-status</artifactId>
+ <packaging>jar</packaging>
+ <version>0.0.1-SNAPSHOT</version>
+ <name>DNBD3 Status Monitor</name>
+
+ <properties>
+ <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+ <maven.test.skip>true</maven.test.skip>
+ </properties>
+
+ <build>
+ <plugins>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-compiler-plugin</artifactId>
+ <version>3.1</version>
+ <configuration>
+ <source>1.7</source>
+ <target>1.7</target>
+ <optimize>true</optimize>
+ </configuration>
+ </plugin>
+ <plugin>
+ <artifactId>maven-assembly-plugin</artifactId>
+ <executions>
+ <execution>
+ <phase>package</phase>
+ <goals>
+ <goal>single</goal>
+ </goals>
+ </execution>
+ </executions>
+ <configuration>
+ <archive>
+ <manifest>
+ <mainClass>org.openslx.dnbd3.status.App</mainClass>
+ </manifest>
+ </archive>
+
+ <descriptorRefs>
+ <descriptorRef>jar-with-dependencies</descriptorRef>
+ </descriptorRefs>
+ </configuration>
+ </plugin>
+ </plugins>
+ </build>
+ <dependencies>
+ <dependency>
+ <groupId>com.google.code.gson</groupId>
+ <artifactId>gson</artifactId>
+ <version>2.2.4</version>
+ <scope>compile</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.commons</groupId>
+ <artifactId>commons-math3</artifactId>
+ <version>3.5</version>
+ </dependency>
+ <dependency>
+ <groupId>ar.com.hjg</groupId>
+ <artifactId>pngj</artifactId>
+ <version>2.1.0</version>
+ </dependency>
+ </dependencies>
+</project> \ No newline at end of file
diff --git a/src/main/java/fi/iki/elonen/NanoHTTPD.java b/src/main/java/fi/iki/elonen/NanoHTTPD.java
new file mode 100644
index 0000000..822a6bb
--- /dev/null
+++ b/src/main/java/fi/iki/elonen/NanoHTTPD.java
@@ -0,0 +1,1098 @@
+package fi.iki.elonen;
+
+/*
+ * #%L
+ * NanoHttpd-Core
+ * %%
+ * Copyright (C) 2012 - 2015 nanohttpd
+ * %%
+ * Redistribution and use in source and binary forms, with or without modification,
+ * are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. 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.
+ *
+ * 3. Neither the name of the nanohttpd 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 COPYRIGHT HOLDERS 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 COPYRIGHT HOLDER OR 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.
+ * #L%
+ */
+
+import java.io.BufferedReader;
+import java.io.ByteArrayInputStream;
+import java.io.Closeable;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.OutputStream;
+import java.io.PrintWriter;
+import java.io.PushbackInputStream;
+import java.io.Reader;
+import java.io.UnsupportedEncodingException;
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.net.ServerSocket;
+import java.net.Socket;
+import java.net.SocketException;
+import java.net.SocketTimeoutException;
+import java.net.URLDecoder;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Set;
+import java.util.StringTokenizer;
+import java.util.TimeZone;
+import java.util.concurrent.ArrayBlockingQueue;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.RejectedExecutionException;
+import java.util.concurrent.ThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * A simple, tiny, nicely embeddable HTTP server in Java
+ * <p/>
+ * <p/>
+ * NanoHTTPD
+ * <p>
+ * Copyright (c) 2012-2013 by Paul S. Hawke, 2001,2005-2013 by Jarno Elonen, 2010 by Konstantinos
+ * Togias
+ * </p>
+ * <p/>
+ * <p/>
+ * <b>Features + limitations: </b>
+ * <ul>
+ * <p/>
+ * <li>Only one Java file</li>
+ * <li>Java 5 compatible</li>
+ * <li>Released as open source, Modified BSD licence</li>
+ * <li>No fixed config files, logging, authorization etc. (Implement yourself if you need them.)</li>
+ * <li>Supports parameter parsing of GET and POST methods (+ rudimentary PUT support in 1.25)</li>
+ * <li>Supports both dynamic content and file serving</li>
+ * <li>Supports file upload (since version 1.2, 2010)</li>
+ * <li>Supports partial content (streaming)</li>
+ * <li>Supports ETags</li>
+ * <li>Never caches anything</li>
+ * <li>Doesn't limit bandwidth, request time or simultaneous connections</li>
+ * <li>Default code serves files and shows all HTTP parameters and headers</li>
+ * <li>File server supports directory listing, index.html and index.htm</li>
+ * <li>File server supports partial content (streaming)</li>
+ * <li>File server supports ETags</li>
+ * <li>File server does the 301 redirection trick for directories without '/'</li>
+ * <li>File server supports simple skipping for files (continue download)</li>
+ * <li>File server serves also very long files without memory overhead</li>
+ * <li>Contains a built-in list of most common MIME types</li>
+ * <li>All header names are converted to lower case so they don't vary between browsers/clients</li>
+ * <p/>
+ * </ul>
+ * <p/>
+ * <p/>
+ * <b>How to use: </b>
+ * <ul>
+ * <p/>
+ * <li>Subclass and implement serve() and embed to your own program</li>
+ * <p/>
+ * </ul>
+ * <p/>
+ * See the separate "LICENSE.md" file for the distribution license (Modified BSD licence)
+ */
+public abstract class NanoHTTPD implements Runnable
+{
+ /**
+ * Maximum time to wait on Socket.getInputStream().read() (in milliseconds)
+ * This is required as the Keep-Alive HTTP connections would otherwise
+ * block the socket reading thread forever (or as long the browser is open).
+ */
+ public static final int SOCKET_READ_TIMEOUT = 15000;
+ /**
+ * Common MIME type for dynamic content: plain text
+ */
+ public static final String MIME_PLAINTEXT = "text/plain";
+ /**
+ * Common MIME type for dynamic content: html
+ */
+ public static final String MIME_HTML = "text/html";
+ /**
+ * Pseudo-Parameter to use to store the actual query string in the parameters map for later
+ * re-processing.
+ */
+ private static final String QUERY_STRING_PARAMETER = "NanoHttpd.QUERY_STRING";
+ private final String hostname;
+ private final int myPort;
+ private ServerSocket myServerSocket;
+ private Set<Socket> openConnections = new HashSet<Socket>();
+ /**
+ * Pluggable strategy for asynchronously executing requests.
+ */
+ private AsyncRunner asyncRunner;
+
+ /**
+ * Constructs an HTTP server on given port.
+ */
+ public NanoHTTPD( int port )
+ {
+ this( null, port );
+ }
+
+ /**
+ * Constructs an HTTP server on given hostname and port.
+ */
+ public NanoHTTPD( String hostname, int port )
+ {
+ this.hostname = hostname;
+ this.myPort = port;
+ setAsyncRunner( new DefaultAsyncRunner() );
+ }
+
+ protected static final void safeClose( Closeable closeable )
+ {
+ if ( closeable != null ) {
+ try {
+ closeable.close();
+ } catch ( IOException e ) {
+ }
+ }
+ }
+
+ /**
+ * Start the server.
+ *
+ * @throws IOException if the socket is in use.
+ */
+ @Override
+ public void run()
+ {
+ try {
+ myServerSocket = new ServerSocket();
+ myServerSocket.setReuseAddress( true );
+ myServerSocket.bind( ( hostname != null ) ? new InetSocketAddress( hostname, myPort ) : new InetSocketAddress( myPort ) );
+ } catch ( Exception e ) {
+ throw new RuntimeException( e );
+ }
+
+ do {
+ try {
+ final Socket finalAccept = myServerSocket.accept();
+ registerConnection( finalAccept );
+ finalAccept.setSoTimeout( SOCKET_READ_TIMEOUT );
+ final InputStream inputStream = finalAccept.getInputStream();
+ asyncRunner.exec( new Runnable() {
+ @Override
+ public void run()
+ {
+ OutputStream outputStream = null;
+ try {
+ outputStream = finalAccept.getOutputStream();
+ HTTPSession session = new HTTPSession( inputStream, outputStream, finalAccept.getInetAddress() );
+ while ( !finalAccept.isClosed() ) {
+ session.execute();
+ }
+ } catch ( Exception e ) {
+ // When the socket is closed by the client, we throw our own SocketException
+ // to break the "keep alive" loop above.
+ if ( ! ( e instanceof SocketException && "NanoHttpd Shutdown".equals( e.getMessage() ) ) ) {
+ e.printStackTrace();
+ }
+ } finally {
+ safeClose( outputStream );
+ safeClose( inputStream );
+ safeClose( finalAccept );
+ unRegisterConnection( finalAccept );
+ }
+ }
+ } );
+ } catch ( IOException e ) {
+ }
+ } while ( !myServerSocket.isClosed() );
+ }
+
+ /**
+ * Stop the server.
+ */
+ public void stop()
+ {
+ try {
+ safeClose( myServerSocket );
+ closeAllConnections();
+ } catch ( Exception e ) {
+ e.printStackTrace();
+ }
+ }
+
+ /**
+ * Registers that a new connection has been set up.
+ *
+ * @param socket the {@link Socket} for the connection.
+ */
+ public synchronized void registerConnection( Socket socket )
+ {
+ openConnections.add( socket );
+ }
+
+ /**
+ * Registers that a connection has been closed
+ *
+ * @param socket
+ * the {@link Socket} for the connection.
+ */
+ public synchronized void unRegisterConnection( Socket socket )
+ {
+ openConnections.remove( socket );
+ }
+
+ /**
+ * Forcibly closes all connections that are open.
+ */
+ public synchronized void closeAllConnections()
+ {
+ for ( Socket socket : openConnections ) {
+ safeClose( socket );
+ }
+ }
+
+ public final int getListeningPort()
+ {
+ return myServerSocket == null ? -1 : myServerSocket.getLocalPort();
+ }
+
+ public final boolean wasStarted()
+ {
+ return myServerSocket != null;
+ }
+
+ public final boolean isAlive()
+ {
+ return wasStarted() && !myServerSocket.isClosed();
+ }
+
+ /**
+ * Override this to customize the server.
+ * <p/>
+ * <p/>
+ * (By default, this returns a 404 "Not Found" plain text error response.)
+ *
+ * @param uri Percent-decoded URI without parameters, for example "/index.cgi"
+ * @param method "GET", "POST" etc.
+ * @param parms Parsed, percent decoded parameters from URI and, in case of POST, data.
+ * @param headers Header entries, percent decoded
+ * @return HTTP response, see class Response for details
+ */
+ @Deprecated
+ public Response serve( String uri, Method method, Map<String, String> headers, Map<String, String> parms,
+ Map<String, String> files )
+ {
+ return new Response( Response.Status.NOT_FOUND, MIME_PLAINTEXT, "Not Found" );
+ }
+
+ /**
+ * Override this to customize the server.
+ * <p/>
+ * <p/>
+ * (By default, this returns a 404 "Not Found" plain text error response.)
+ *
+ * @param session The HTTP session
+ * @return HTTP response, see class Response for details
+ */
+ public Response serve( IHTTPSession session )
+ {
+ Map<String, String> files = new HashMap<String, String>();
+ Method method = session.getMethod();
+ if ( Method.PUT.equals( method ) || Method.POST.equals( method ) ) {
+ try {
+ session.parseBody( files );
+ } catch ( IOException ioe ) {
+ return new Response( Response.Status.INTERNAL_ERROR, MIME_PLAINTEXT, "SERVER INTERNAL ERROR: IOException: " + ioe.getMessage() );
+ } catch ( ResponseException re ) {
+ return new Response( re.getStatus(), MIME_PLAINTEXT, re.getMessage() );
+ }
+ }
+
+ Map<String, String> parms = session.getParms();
+ parms.put( QUERY_STRING_PARAMETER, session.getQueryParameterString() );
+ return serve( session.getUri(), method, session.getHeaders(), parms, files );
+ }
+
+ /**
+ * Decode percent encoded <code>String</code> values.
+ *
+ * @param str the percent encoded <code>String</code>
+ * @return expanded form of the input, for example "foo%20bar" becomes "foo bar"
+ */
+ protected String decodePercent( String str )
+ {
+ String decoded = null;
+ try {
+ decoded = URLDecoder.decode( str, "UTF8" );
+ } catch ( UnsupportedEncodingException ignored ) {
+ }
+ return decoded;
+ }
+
+ /**
+ * Decode parameters from a URL, handing the case where a single parameter name might have been
+ * supplied several times, by return lists of values. In general these lists will contain a
+ * single
+ * element.
+ *
+ * @param parms original <b>NanoHTTPD</b> parameters values, as passed to the
+ * <code>serve()</code> method.
+ * @return a map of <code>String</code> (parameter name) to <code>List&lt;String&gt;</code> (a
+ * list of the values supplied).
+ */
+ protected Map<String, List<String>> decodeParameters( Map<String, String> parms )
+ {
+ return this.decodeParameters( parms.get( QUERY_STRING_PARAMETER ) );
+ }
+
+ /**
+ * Decode parameters from a URL, handing the case where a single parameter name might have been
+ * supplied several times, by return lists of values. In general these lists will contain a
+ * single
+ * element.
+ *
+ * @param queryString a query string pulled from the URL.
+ * @return a map of <code>String</code> (parameter name) to <code>List&lt;String&gt;</code> (a
+ * list of the values supplied).
+ */
+ protected Map<String, List<String>> decodeParameters( String queryString )
+ {
+ Map<String, List<String>> parms = new HashMap<String, List<String>>();
+ if ( queryString != null ) {
+ StringTokenizer st = new StringTokenizer( queryString, "&" );
+ while ( st.hasMoreTokens() ) {
+ String e = st.nextToken();
+ int sep = e.indexOf( '=' );
+ String propertyName = ( sep >= 0 ) ? decodePercent( e.substring( 0, sep ) ).trim() : decodePercent( e ).trim();
+ if ( !parms.containsKey( propertyName ) ) {
+ parms.put( propertyName, new ArrayList<String>() );
+ }
+ String propertyValue = ( sep >= 0 ) ? decodePercent( e.substring( sep + 1 ) ) : null;
+ if ( propertyValue != null ) {
+ parms.get( propertyName ).add( propertyValue );
+ }
+ }
+ }
+ return parms;
+ }
+
+ // ------------------------------------------------------------------------------- //
+ //
+ // Threading Strategy.
+ //
+ // ------------------------------------------------------------------------------- //
+
+ /**
+ * Pluggable strategy for asynchronously executing requests.
+ *
+ * @param asyncRunner new strategy for handling threads.
+ */
+ public void setAsyncRunner( AsyncRunner asyncRunner )
+ {
+ this.asyncRunner = asyncRunner;
+ }
+
+ /**
+ * HTTP Request methods, with the ability to decode a <code>String</code> back to its enum value.
+ */
+ public enum Method
+ {
+ GET, PUT, POST, DELETE, HEAD, OPTIONS;
+
+ static Method lookup( String method )
+ {
+ for ( Method m : Method.values() ) {
+ if ( m.toString().equalsIgnoreCase( method ) ) {
+ return m;
+ }
+ }
+ return null;
+ }
+ }
+
+ /**
+ * Pluggable strategy for asynchronously executing requests.
+ */
+ public interface AsyncRunner
+ {
+ void exec( Runnable code );
+ }
+
+ // ------------------------------------------------------------------------------- //
+
+ /**
+ * Default threading strategy for NanoHTTPD.
+ * <p/>
+ * <p>
+ * By default, the server spawns a new Thread for every incoming request. These are set to
+ * <i>daemon</i> status, and named according to the request number. The name is useful when
+ * profiling the application.
+ * </p>
+ */
+ public static class DefaultAsyncRunner implements AsyncRunner
+ {
+ private long requestCount;
+ private ExecutorService pool = new ThreadPoolExecutor( 4, 32, 1, TimeUnit.MINUTES, new ArrayBlockingQueue<Runnable>( 2 ) );
+
+ @Override
+ public void exec( Runnable code )
+ {
+ try {
+ pool.submit( code );
+ ++requestCount;
+ System.out.println( "NanoHttpd Request Processor (#" + requestCount + ")" );
+ } catch ( RejectedExecutionException e ) {
+ System.out.println( "Too many pending requests, dropping client..." );
+ }
+ }
+ }
+
+ /**
+ * HTTP response. Return one of these from serve().
+ */
+ public static class Response
+ {
+ /**
+ * HTTP status code after processing, e.g. "200 OK", Status.OK
+ */
+ private IStatus status;
+ /**
+ * MIME type of content, e.g. "text/html"
+ */
+ private String mimeType;
+ /**
+ * Data of the response, may be null.
+ */
+ private InputStream data;
+ /**
+ * Headers for the HTTP response. Use addHeader() to add lines.
+ */
+ private Map<String, String> header = new HashMap<String, String>();
+ /**
+ * The request method that spawned this response.
+ */
+ private Method requestMethod;
+ /**
+ * Use chunkedTransfer
+ */
+ private boolean chunkedTransfer;
+
+ /**
+ * Default constructor: response = Status.OK, mime = MIME_HTML and your supplied message
+ */
+ public Response( String msg )
+ {
+ this( Status.OK, MIME_HTML, msg );
+ }
+
+ /**
+ * Basic constructor.
+ */
+ public Response( IStatus status, String mimeType, InputStream data )
+ {
+ this.status = status;
+ this.mimeType = mimeType;
+ this.data = data;
+ }
+
+ /**
+ * Convenience method that makes an InputStream out of given text.
+ */
+ public Response( IStatus status, String mimeType, String txt )
+ {
+ this.status = status;
+ this.mimeType = mimeType;
+ try {
+ this.data = txt != null ? new ByteArrayInputStream( txt.getBytes( "UTF-8" ) ) : null;
+ } catch ( java.io.UnsupportedEncodingException uee ) {
+ uee.printStackTrace();
+ }
+ }
+
+ /**
+ * Adds given line to the header.
+ */
+ public void addHeader( String name, String value )
+ {
+ header.put( name, value );
+ }
+
+ public String getHeader( String name )
+ {
+ return header.get( name );
+ }
+
+ /**
+ * Sends given response to the socket.
+ */
+ protected void send( OutputStream outputStream )
+ {
+ String mime = mimeType;
+ SimpleDateFormat gmtFrmt = new SimpleDateFormat( "E, d MMM yyyy HH:mm:ss 'GMT'", Locale.US );
+ gmtFrmt.setTimeZone( TimeZone.getTimeZone( "GMT" ) );
+
+ try {
+ if ( status == null ) {
+ throw new Error( "sendResponse(): Status can't be null." );
+ }
+ PrintWriter pw = new PrintWriter( outputStream );
+ pw.print( "HTTP/1.1 " + status.getDescription() + " \r\n" );
+
+ if ( mime != null ) {
+ pw.print( "Content-Type: " + mime + "\r\n" );
+ }
+
+ if ( header == null || header.get( "Date" ) == null ) {
+ pw.print( "Date: " + gmtFrmt.format( new Date() ) + "\r\n" );
+ }
+
+ if ( header != null ) {
+ for ( String key : header.keySet() ) {
+ String value = header.get( key );
+ pw.print( key + ": " + value + "\r\n" );
+ }
+ }
+
+ sendConnectionHeaderIfNotAlreadyPresent( pw, header );
+
+ if ( requestMethod != Method.HEAD && chunkedTransfer ) {
+ sendAsChunked( outputStream, pw );
+ } else {
+ int pending = data != null ? data.available() : 0;
+ pending = sendContentLengthHeaderIfNotAlreadyPresent( pw, header, pending );
+ pw.print( "\r\n" );
+ pw.flush();
+ sendAsFixedLength( outputStream, pending );
+ }
+ outputStream.flush();
+ safeClose( data );
+ } catch ( IOException ioe ) {
+ // Couldn't write? No can do.
+ }
+ }
+
+ protected int sendContentLengthHeaderIfNotAlreadyPresent( PrintWriter pw, Map<String, String> header, int size )
+ {
+ for ( String headerName : header.keySet() ) {
+ if ( headerName.equalsIgnoreCase( "content-length" ) ) {
+ try {
+ return Integer.parseInt( header.get( headerName ) );
+ } catch ( NumberFormatException ex ) {
+ return size;
+ }
+ }
+ }
+
+ pw.print( "Content-Length: " + size + "\r\n" );
+ return size;
+ }
+
+ protected void sendConnectionHeaderIfNotAlreadyPresent( PrintWriter pw, Map<String, String> header )
+ {
+ if ( !headerAlreadySent( header, "connection" ) ) {
+ pw.print( "Connection: keep-alive\r\n" );
+ }
+ }
+
+ private boolean headerAlreadySent( Map<String, String> header, String name )
+ {
+ boolean alreadySent = false;
+ for ( String headerName : header.keySet() ) {
+ alreadySent |= headerName.equalsIgnoreCase( name );
+ }
+ return alreadySent;
+ }
+
+ private void sendAsChunked( OutputStream outputStream, PrintWriter pw ) throws IOException
+ {
+ pw.print( "Transfer-Encoding: chunked\r\n" );
+ pw.print( "\r\n" );
+ pw.flush();
+ int BUFFER_SIZE = 16 * 1024;
+ byte[] CRLF = "\r\n".getBytes();
+ byte[] buff = new byte[ BUFFER_SIZE ];
+ int read;
+ while ( ( read = data.read( buff ) ) > 0 ) {
+ outputStream.write( String.format( "%x\r\n", read ).getBytes() );
+ outputStream.write( buff, 0, read );
+ outputStream.write( CRLF );
+ }
+ outputStream.write( String.format( "0\r\n\r\n" ).getBytes() );
+ }
+
+ private void sendAsFixedLength( OutputStream outputStream, int pending ) throws IOException
+ {
+ if ( requestMethod != Method.HEAD && data != null ) {
+ int BUFFER_SIZE = 16 * 1024;
+ byte[] buff = new byte[ BUFFER_SIZE ];
+ while ( pending > 0 ) {
+ int read = data.read( buff, 0, ( ( pending > BUFFER_SIZE ) ? BUFFER_SIZE : pending ) );
+ if ( read <= 0 ) {
+ break;
+ }
+ outputStream.write( buff, 0, read );
+ pending -= read;
+ }
+ }
+ }
+
+ public IStatus getStatus()
+ {
+ return status;
+ }
+
+ public void setStatus( IStatus status )
+ {
+ this.status = status;
+ }
+
+ public String getMimeType()
+ {
+ return mimeType;
+ }
+
+ public void setMimeType( String mimeType )
+ {
+ this.mimeType = mimeType;
+ }
+
+ public InputStream getData()
+ {
+ return data;
+ }
+
+ public void setData( InputStream data )
+ {
+ this.data = data;
+ }
+
+ public Method getRequestMethod()
+ {
+ return requestMethod;
+ }
+
+ public void setRequestMethod( Method requestMethod )
+ {
+ this.requestMethod = requestMethod;
+ }
+
+ public void setChunkedTransfer( boolean chunkedTransfer )
+ {
+ this.chunkedTransfer = chunkedTransfer;
+ }
+
+ public interface IStatus
+ {
+ int getRequestStatus();
+
+ String getDescription();
+ }
+
+ /**
+ * Some HTTP response status codes
+ */
+ public enum Status implements IStatus
+ {
+ SWITCH_PROTOCOL( 101, "Switching Protocols" ), OK( 200, "OK" ), CREATED( 201, "Created" ), ACCEPTED( 202, "Accepted" ), NO_CONTENT( 204, "No Content" ), PARTIAL_CONTENT(
+ 206, "Partial Content" ), REDIRECT( 301,
+ "Moved Permanently" ), NOT_MODIFIED( 304, "Not Modified" ), BAD_REQUEST( 400, "Bad Request" ), UNAUTHORIZED( 401,
+ "Unauthorized" ), FORBIDDEN( 403, "Forbidden" ), NOT_FOUND( 404, "Not Found" ), METHOD_NOT_ALLOWED( 405, "Method Not Allowed" ), RANGE_NOT_SATISFIABLE( 416,
+ "Requested Range Not Satisfiable" ), INTERNAL_ERROR( 500, "Internal Server Error" );
+ private final int requestStatus;
+ private final String description;
+
+ Status( int requestStatus, String description )
+ {
+ this.requestStatus = requestStatus;
+ this.description = description;
+ }
+
+ @Override
+ public int getRequestStatus()
+ {
+ return this.requestStatus;
+ }
+
+ @Override
+ public String getDescription()
+ {
+ return "" + this.requestStatus + " " + description;
+ }
+ }
+ }
+
+ public static final class ResponseException extends Exception
+ {
+ private static final long serialVersionUID = 6569838532917408380L;
+ private final Response.Status status;
+
+ public ResponseException( Response.Status status, String message )
+ {
+ super( message );
+ this.status = status;
+ }
+
+ public ResponseException( Response.Status status, String message, Exception e )
+ {
+ super( message, e );
+ this.status = status;
+ }
+
+ public Response.Status getStatus()
+ {
+ return status;
+ }
+ }
+
+ /**
+ * Handles one session, i.e. parses the HTTP request and returns the response.
+ */
+ public interface IHTTPSession
+ {
+ void execute() throws IOException;
+
+ Map<String, String> getParms();
+
+ Map<String, String> getHeaders();
+
+ /**
+ * @return the path part of the URL.
+ */
+ String getUri();
+
+ String getQueryParameterString();
+
+ Method getMethod();
+
+ InputStream getInputStream();
+
+ /**
+ * Adds the files in the request body to the files map.
+ *
+ * @param files map to modify
+ */
+ void parseBody( Map<String, String> files ) throws IOException, ResponseException;
+ }
+
+ protected class HTTPSession implements IHTTPSession
+ {
+ public static final int BUFSIZE = 8192;
+ private final OutputStream outputStream;
+ private PushbackInputStream inputStream;
+ private int splitbyte;
+ private int rlen;
+ private String uri;
+ private Method method;
+ private Map<String, String> parms;
+ private Map<String, String> headers;
+ private String queryParameterString;
+ private String remoteIp;
+
+ public HTTPSession( InputStream inputStream, OutputStream outputStream )
+ {
+ this.inputStream = new PushbackInputStream( inputStream, BUFSIZE );
+ this.outputStream = outputStream;
+ }
+
+ public HTTPSession( InputStream inputStream, OutputStream outputStream, InetAddress inetAddress )
+ {
+ this.inputStream = new PushbackInputStream( inputStream, BUFSIZE );
+ this.outputStream = outputStream;
+ remoteIp = inetAddress.isLoopbackAddress() || inetAddress.isAnyLocalAddress() ? "127.0.0.1" : inetAddress.getHostAddress().toString();
+ headers = new HashMap<String, String>();
+ }
+
+ @Override
+ public void execute() throws IOException
+ {
+ try {
+ // Read the first 8192 bytes.
+ // The full header should fit in here.
+ // Apache's default header limit is 8KB.
+ // Do NOT assume that a single read will get the entire header at once!
+ byte[] buf = new byte[ BUFSIZE ];
+ splitbyte = 0;
+ rlen = 0;
+ {
+ int read = -1;
+ try {
+ read = inputStream.read( buf, 0, BUFSIZE );
+ } catch ( Exception e ) {
+ safeClose( inputStream );
+ safeClose( outputStream );
+ throw new SocketException( "NanoHttpd Shutdown" );
+ }
+ if ( read == -1 ) {
+ // socket was been closed
+ safeClose( inputStream );
+ safeClose( outputStream );
+ throw new SocketException( "NanoHttpd Shutdown" );
+ }
+ while ( read > 0 ) {
+ rlen += read;
+ splitbyte = findHeaderEnd( buf, rlen );
+ if ( splitbyte > 0 )
+ break;
+ read = inputStream.read( buf, rlen, BUFSIZE - rlen );
+ }
+ }
+
+ if ( splitbyte < rlen ) {
+ inputStream.unread( buf, splitbyte, rlen - splitbyte );
+ }
+
+ parms = new HashMap<String, String>();
+ if ( null == headers ) {
+ headers = new HashMap<String, String>();
+ } else {
+ headers.clear();
+ }
+
+ if ( null != remoteIp ) {
+ headers.put( "remote-addr", remoteIp );
+ headers.put( "http-client-ip", remoteIp );
+ }
+
+ // Create a BufferedReader for parsing the header.
+ BufferedReader hin = new BufferedReader( new InputStreamReader( new ByteArrayInputStream( buf, 0, rlen ) ) );
+
+ // Decode the header into parms and header java properties
+ Map<String, String> pre = new HashMap<String, String>();
+ decodeHeader( hin, pre, parms, headers );
+
+ method = Method.lookup( pre.get( "method" ) );
+ if ( method == null ) {
+ throw new ResponseException( Response.Status.BAD_REQUEST, "BAD REQUEST: Syntax error." );
+ }
+
+ uri = pre.get( "uri" );
+
+ // Ok, now do the serve()
+ Response r = serve( this );
+ if ( r == null ) {
+ throw new ResponseException( Response.Status.INTERNAL_ERROR, "SERVER INTERNAL ERROR: Serve() returned a null response." );
+ } else {
+ r.setRequestMethod( method );
+ r.send( outputStream );
+ }
+ } catch ( SocketException e ) {
+ // throw it out to close socket object (finalAccept)
+ throw e;
+ } catch ( SocketTimeoutException ste ) {
+ throw ste;
+ } catch ( IOException ioe ) {
+ Response r = new Response( Response.Status.INTERNAL_ERROR, MIME_PLAINTEXT, "SERVER INTERNAL ERROR: IOException: " + ioe.getMessage() );
+ r.send( outputStream );
+ safeClose( outputStream );
+ } catch ( ResponseException re ) {
+ Response r = new Response( re.getStatus(), MIME_PLAINTEXT, re.getMessage() );
+ r.send( outputStream );
+ safeClose( outputStream );
+ }
+ }
+
+ @Override
+ public void parseBody( Map<String, String> files ) throws IOException, ResponseException
+ {
+ final Reader in = new InputStreamReader( inputStream );
+ long size;
+ if ( headers.containsKey( "content-length" ) ) {
+ size = Integer.parseInt( headers.get( "content-length" ) );
+ } else if ( splitbyte < rlen ) {
+ size = rlen - splitbyte;
+ } else {
+ size = 0;
+ }
+
+ // If the method is POST, there may be parameters
+ // in data section, too, read it:
+ if ( Method.POST.equals( method ) ) {
+ String contentType = "";
+ String contentTypeHeader = headers.get( "content-type" );
+
+ StringTokenizer st = null;
+ if ( contentTypeHeader != null ) {
+ st = new StringTokenizer( contentTypeHeader, ",; " );
+ if ( st.hasMoreTokens() ) {
+ contentType = st.nextToken();
+ }
+ }
+
+ if ( "multipart/form-data".equalsIgnoreCase( contentType ) ) {
+ throw new ResponseException( Response.Status.BAD_REQUEST, "BAD REQUEST: Content type is multipart/form-data, which is not supported" );
+ } else {
+ String postLine = "";
+ StringBuilder postLineBuffer = new StringBuilder();
+ char pbuf[] = new char[ 512 ];
+ while ( rlen >= 0 && size > 0 && !postLine.endsWith( "\r\n" ) ) {
+ rlen = in.read( pbuf, 0, (int)Math.min( size, 512 ) );
+ if ( rlen <= 0 )
+ break;
+ postLine = String.valueOf( pbuf, 0, rlen );
+ postLineBuffer.append( postLine );
+ }
+ postLine = postLineBuffer.toString().trim();
+ // Handle application/x-www-form-urlencoded
+ if ( "application/x-www-form-urlencoded".equalsIgnoreCase( contentType ) ) {
+ decodeParms( postLine, parms );
+ } else if ( postLine.length() != 0 ) {
+ // Special case for raw POST data => create a special files entry "postData" with raw content data
+ files.put( "postData", postLine );
+ }
+ }
+ }
+ }
+
+ /**
+ * Decodes the sent headers and loads the data into Key/value pairs
+ */
+ private void decodeHeader( BufferedReader in, Map<String, String> pre, Map<String, String> parms, Map<String, String> headers )
+ throws ResponseException
+ {
+ try {
+ // Read the request line
+ String inLine = in.readLine();
+ if ( inLine == null ) {
+ return;
+ }
+
+ StringTokenizer st = new StringTokenizer( inLine );
+ if ( !st.hasMoreTokens() ) {
+ throw new ResponseException( Response.Status.BAD_REQUEST, "BAD REQUEST: Syntax error. Usage: GET /example/file.html" );
+ }
+
+ pre.put( "method", st.nextToken() );
+
+ if ( !st.hasMoreTokens() ) {
+ throw new ResponseException( Response.Status.BAD_REQUEST, "BAD REQUEST: Missing URI. Usage: GET /example/file.html" );
+ }
+
+ String uri = st.nextToken();
+
+ // Decode parameters from the URI
+ int qmi = uri.indexOf( '?' );
+ if ( qmi >= 0 ) {
+ decodeParms( uri.substring( qmi + 1 ), parms );
+ uri = decodePercent( uri.substring( 0, qmi ) );
+ } else {
+ uri = decodePercent( uri );
+ }
+
+ // If there's another token, its protocol version,
+ // followed by HTTP headers. Ignore version but parse headers.
+ // NOTE: this now forces header names lower case since they are
+ // case insensitive and vary by client.
+ if ( st.hasMoreTokens() ) {
+ String line = in.readLine();
+ while ( line != null && line.trim().length() > 0 ) {
+ int p = line.indexOf( ':' );
+ if ( p >= 0 )
+ headers.put( line.substring( 0, p ).trim().toLowerCase( Locale.US ), line.substring( p + 1 ).trim() );
+ line = in.readLine();
+ }
+ }
+
+ pre.put( "uri", uri );
+ } catch ( IOException ioe ) {
+ throw new ResponseException( Response.Status.INTERNAL_ERROR, "SERVER INTERNAL ERROR: IOException: " + ioe.getMessage(), ioe );
+ }
+ }
+
+ /**
+ * Find byte index separating header from body. It must be the last byte of the first two
+ * sequential new lines.
+ */
+ private int findHeaderEnd( final byte[] buf, int rlen )
+ {
+ int splitbyte = 0;
+ while ( splitbyte + 3 < rlen ) {
+ if ( buf[splitbyte] == '\r' && buf[splitbyte + 1] == '\n' && buf[splitbyte + 2] == '\r' && buf[splitbyte + 3] == '\n' ) {
+ return splitbyte + 4;
+ }
+ splitbyte++;
+ }
+ return 0;
+ }
+
+ /**
+ * Decodes parameters in percent-encoded URI-format ( e.g.
+ * "name=Jack%20Daniels&pass=Single%20Malt" ) and
+ * adds them to given Map. NOTE: this doesn't support multiple identical keys due to the
+ * simplicity of Map.
+ */
+ private void decodeParms( String parms, Map<String, String> p )
+ {
+ if ( parms == null ) {
+ queryParameterString = "";
+ return;
+ }
+
+ queryParameterString = parms;
+ StringTokenizer st = new StringTokenizer( parms, "&" );
+ while ( st.hasMoreTokens() ) {
+ String e = st.nextToken();
+ int sep = e.indexOf( '=' );
+ if ( sep >= 0 ) {
+ p.put( decodePercent( e.substring( 0, sep ) ).trim(),
+ decodePercent( e.substring( sep + 1 ) ) );
+ } else {
+ p.put( decodePercent( e ).trim(), "" );
+ }
+ }
+ }
+
+ @Override
+ public final Map<String, String> getParms()
+ {
+ return parms;
+ }
+
+ public String getQueryParameterString()
+ {
+ return queryParameterString;
+ }
+
+ @Override
+ public final Map<String, String> getHeaders()
+ {
+ return headers;
+ }
+
+ @Override
+ public final String getUri()
+ {
+ return uri;
+ }
+
+ @Override
+ public final Method getMethod()
+ {
+ return method;
+ }
+
+ @Override
+ public final InputStream getInputStream()
+ {
+ return inputStream;
+ }
+ }
+
+}
diff --git a/src/main/java/org/openslx/dnbd3/status/App.java b/src/main/java/org/openslx/dnbd3/status/App.java
new file mode 100644
index 0000000..9f7ef5f
--- /dev/null
+++ b/src/main/java/org/openslx/dnbd3/status/App.java
@@ -0,0 +1,14 @@
+package org.openslx.dnbd3.status;
+
+import java.io.IOException;
+
+public class App
+{
+
+ public static void main( String[] args ) throws IOException
+ {
+ WebServer ws = new WebServer( 8888 );
+ ws.run();
+ }
+
+}
diff --git a/src/main/java/org/openslx/dnbd3/status/StatisticsGenerator.java b/src/main/java/org/openslx/dnbd3/status/StatisticsGenerator.java
new file mode 100644
index 0000000..777ce7a
--- /dev/null
+++ b/src/main/java/org/openslx/dnbd3/status/StatisticsGenerator.java
@@ -0,0 +1,146 @@
+package org.openslx.dnbd3.status;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.ArrayBlockingQueue;
+import java.util.concurrent.Callable;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Future;
+import java.util.concurrent.ThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
+
+import org.openslx.dnbd3.status.output.EdgeSerializer;
+import org.openslx.dnbd3.status.output.OutputMain;
+import org.openslx.dnbd3.status.output.ServerStats;
+import org.openslx.dnbd3.status.poller.ServerPoller;
+import org.openslx.dnbd3.status.rpc.Client;
+import org.openslx.dnbd3.status.rpc.Status;
+import org.openslx.graph.ClientNode;
+import org.openslx.graph.Edge;
+import org.openslx.graph.Graph;
+import org.openslx.graph.Node;
+import org.openslx.graph.ServerNode;
+
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+
+public class StatisticsGenerator
+{
+ private final List<ServerPoller> pollers;
+ private volatile long lastUpdate = 0;
+ private ExecutorService threadPool = new ThreadPoolExecutor( 1, 6, 1, TimeUnit.MINUTES, new ArrayBlockingQueue<Runnable>( 100 ) );
+ private List<Future<Status>> futureStatusList = new ArrayList<>();
+ private List<Status> statusList = new ArrayList<>();
+ private final Gson jsonBuilder;
+
+ private final Graph graph = new Graph( "DNBD 3 Status" );
+ private byte[] imgData = null;
+ private final OutputMain output = new OutputMain();
+
+ public StatisticsGenerator( List<ServerPoller> pollers )
+ {
+ this.pollers = pollers;
+ for ( ServerPoller poller : pollers ) {
+ Node server = new ServerNode( poller.getAddress() );
+ server.activate();
+ graph.addNode( server );
+ }
+ // Prepare json serializer for graph
+ final GsonBuilder gsonBuilder = new GsonBuilder();
+ gsonBuilder.excludeFieldsWithoutExposeAnnotation();
+ gsonBuilder.registerTypeHierarchyAdapter( Edge.class, new EdgeSerializer() );
+ jsonBuilder = gsonBuilder.create();
+ output.graph = graph;
+ output.servers = new ArrayList<>();
+ output.timestamp = 0;
+ }
+
+ private synchronized void updateAll()
+ {
+ futureStatusList.clear();
+ for ( final ServerPoller p : pollers ) {
+ Future<Status> ret = threadPool.submit( new Callable<Status>() {
+ @Override
+ public Status call() throws Exception
+ {
+ return p.update();
+ }
+ } );
+ futureStatusList.add( ret );
+ }
+ statusList.clear();
+ output.servers.clear();
+ for ( Future<Status> future : futureStatusList ) {
+ Status status;
+ try {
+ status = future.get();
+ } catch ( Exception e ) {
+ continue;
+ }
+ if ( status == null )
+ continue;
+ statusList.add( status );
+ ServerStats srv = new ServerStats();
+ srv.address = status.getAddress();
+ srv.bytesReceived = status.getBytesReceived();
+ srv.bytesSent = status.getBytesSent();
+ for ( Client c : status.getClients() ) {
+ srv.bytesSent += c.getBytesSent();
+ }
+ srv.clientCount = status.getClients().size();
+ srv.uptime = status.getUptime();
+ output.servers.add( srv );
+ }
+ output.timestamp = System.currentTimeMillis();
+ synchronized ( graph ) {
+ graph.decay();
+ for ( Status status : statusList ) {
+ String serverIp = status.getAddress();
+ Node server = graph.getNode( serverIp );
+ if ( server == null )
+ server = new ServerNode( serverIp );
+ server.activate();
+ for ( Client client : status.getClients() ) {
+ if ( client.getAddress() == null )
+ continue;
+ String ip = client.getAddress().replaceFirst( "\\:\\d+$", "" );
+ Node n = graph.getNode( ip );
+ if ( n == null )
+ n = new ClientNode( ip );
+ n.activate();
+ graph.setEdge( server, n, ( client.getBytesSent() >> 12 ) + 1 );
+ }
+ }
+ imgData = null;
+ }
+ }
+
+ private synchronized void ensureUpToDate()
+ {
+ final long now = System.currentTimeMillis();
+ if ( now - lastUpdate > 1900 ) {
+ lastUpdate = now;
+ updateAll();
+ }
+ }
+
+ public byte[] getImagePng()
+ {
+ ensureUpToDate();
+ synchronized ( graph ) {
+ if ( imgData == null ) {
+ imgData = graph.makeNextImage();
+ }
+ }
+ return imgData;
+ }
+
+ public String getJson()
+ {
+ ensureUpToDate();
+ synchronized ( graph ) {
+ return jsonBuilder.toJson( output );
+ }
+ }
+
+}
diff --git a/src/main/java/org/openslx/dnbd3/status/WebServer.java b/src/main/java/org/openslx/dnbd3/status/WebServer.java
new file mode 100644
index 0000000..14d9a09
--- /dev/null
+++ b/src/main/java/org/openslx/dnbd3/status/WebServer.java
@@ -0,0 +1,100 @@
+package org.openslx.dnbd3.status;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.InputStream;
+import java.net.URLConnection;
+import java.util.ArrayList;
+import java.util.List;
+
+import org.openslx.dnbd3.status.poller.ServerPoller;
+
+import fi.iki.elonen.NanoHTTPD;
+
+public class WebServer extends NanoHTTPD
+{
+
+ private final StatisticsGenerator imageGenerator;
+
+ public WebServer( int port )
+ {
+ super( port );
+ List<ServerPoller> pollers = new ArrayList<>();
+ pollers.add( new ServerPoller( "132.230.8.113", 5003 ) );
+ pollers.add( new ServerPoller( "132.230.4.60", 5003 ) );
+ pollers.add( new ServerPoller( "132.230.4.1", 5003 ) );
+ imageGenerator = new StatisticsGenerator( pollers );
+ }
+
+ @Override
+ public Response serve( IHTTPSession session )
+ {
+ String uri = session.getUri();
+
+ // Special/dynamic
+ if ( uri.equals( "/image.png" ) )
+ return serveImage();
+ if ( uri.equals( "/data.json" ) )
+ return serveJson();
+
+ // Static files
+ if ( uri.equals( "/" ) )
+ uri = "/index.html";
+
+ File f = new File( "./static/" + uri.replace( "/", "" ) );
+ if ( f.isFile() ) {
+ InputStream is = null;
+ try {
+ is = new FileInputStream( f );
+ /*
+ // TODO: Shit doesn't work
+ return new NanoHTTPD.Response( NanoHTTPD.Response.Status.OK,
+ URLConnection.guessContentTypeFromName( f.getName() ), is );
+ */
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ byte[] buffer = new byte[ 10000 ];
+ for ( ;; ) {
+ int ret = is.read( buffer );
+ if ( ret <= 0 )
+ break;
+ baos.write( buffer, 0, ret );
+ }
+ return new NanoHTTPD.Response( NanoHTTPD.Response.Status.OK,
+ URLConnection.guessContentTypeFromName( f.getName() ), baos.toString( "UTF-8" ) );
+ } catch ( Exception e ) {
+ return new NanoHTTPD.Response( NanoHTTPD.Response.Status.INTERNAL_ERROR,
+ "text/plain", "Internal Server Error" );
+ } finally {
+ safeClose( is );
+ }
+ }
+
+ return new NanoHTTPD.Response( NanoHTTPD.Response.Status.NOT_FOUND, "text/plain", "Nicht gefunden!" );
+ }
+
+ private NanoHTTPD.Response serveImage()
+ {
+ InputStream is = null;
+ byte[] imgData = imageGenerator.getImagePng();
+ if ( imgData != null )
+ is = new ByteArrayInputStream( imgData );
+ if ( is == null ) {
+ return new NanoHTTPD.Response( NanoHTTPD.Response.Status.INTERNAL_ERROR, "text/plain", "Internal Server Error" );
+ } else {
+ return new NanoHTTPD.Response( NanoHTTPD.Response.Status.OK, "image/png", is );
+ }
+ }
+
+ private NanoHTTPD.Response serveJson()
+ {
+ String data = imageGenerator.getJson();
+ if ( data == null ) {
+ return new NanoHTTPD.Response( NanoHTTPD.Response.Status.INTERNAL_ERROR, "text/plain", "Internal Server Error" );
+ } else {
+ return new NanoHTTPD.Response( NanoHTTPD.Response.Status.OK, "application/json", data );
+ }
+ }
+
+}
diff --git a/src/main/java/org/openslx/dnbd3/status/output/EdgeSerializer.java b/src/main/java/org/openslx/dnbd3/status/output/EdgeSerializer.java
new file mode 100644
index 0000000..e014bb3
--- /dev/null
+++ b/src/main/java/org/openslx/dnbd3/status/output/EdgeSerializer.java
@@ -0,0 +1,25 @@
+package org.openslx.dnbd3.status.output;
+
+import java.lang.reflect.Type;
+
+import org.openslx.graph.Edge;
+
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+import com.google.gson.JsonSerializationContext;
+import com.google.gson.JsonSerializer;
+
+public class EdgeSerializer implements JsonSerializer<Edge>
+{
+
+ @Override
+ public JsonElement serialize( Edge src, Type typeOfSrc, JsonSerializationContext context )
+ {
+ final JsonObject out = new JsonObject();
+ out.addProperty( "source", src.getSource().getId() );
+ out.addProperty( "target", src.getTarget().getId() );
+ out.addProperty( "width", src.getWeight() );
+ return out;
+ }
+
+}
diff --git a/src/main/java/org/openslx/dnbd3/status/output/OutputMain.java b/src/main/java/org/openslx/dnbd3/status/output/OutputMain.java
new file mode 100644
index 0000000..c3204e7
--- /dev/null
+++ b/src/main/java/org/openslx/dnbd3/status/output/OutputMain.java
@@ -0,0 +1,19 @@
+package org.openslx.dnbd3.status.output;
+
+import java.util.List;
+
+import org.openslx.graph.Graph;
+
+import com.google.gson.annotations.Expose;
+
+public class OutputMain
+{
+
+ @Expose
+ public Graph graph;
+ @Expose
+ public List<ServerStats> servers;
+ @Expose
+ public long timestamp;
+
+}
diff --git a/src/main/java/org/openslx/dnbd3/status/output/ServerStats.java b/src/main/java/org/openslx/dnbd3/status/output/ServerStats.java
new file mode 100644
index 0000000..c58abb3
--- /dev/null
+++ b/src/main/java/org/openslx/dnbd3/status/output/ServerStats.java
@@ -0,0 +1,19 @@
+package org.openslx.dnbd3.status.output;
+
+import com.google.gson.annotations.Expose;
+
+public class ServerStats
+{
+
+ @Expose
+ public String address;
+ @Expose
+ public int clientCount;
+ @Expose
+ public long uptime;
+ @Expose
+ public long bytesSent;
+ @Expose
+ public long bytesReceived;
+
+}
diff --git a/src/main/java/org/openslx/dnbd3/status/poller/ServerPoller.java b/src/main/java/org/openslx/dnbd3/status/poller/ServerPoller.java
new file mode 100644
index 0000000..bfa70fb
--- /dev/null
+++ b/src/main/java/org/openslx/dnbd3/status/poller/ServerPoller.java
@@ -0,0 +1,70 @@
+package org.openslx.dnbd3.status.poller;
+
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.net.HttpURLConnection;
+import java.net.URL;
+
+import org.openslx.dnbd3.status.rpc.Status;
+
+import com.google.gson.Gson;
+
+/**
+ * Polling a dnbd3 server for its status.
+ *
+ */
+public class ServerPoller
+{
+
+ private final String address;
+ private final String server;
+ private final Gson parseGson = new Gson();
+
+ public ServerPoller( String host, int port )
+ {
+ this.address = host;
+ this.server = "http://" + host + ":" + port + "/";
+ }
+
+ public Status update()
+ {
+ HttpURLConnection con = null;
+ InputStream is;
+ try {
+ con = (HttpURLConnection)new URL( this.server ).openConnection();
+ con.setRequestMethod( "GET" );
+
+ con.setConnectTimeout( 1000 );
+ con.setReadTimeout( 3000 );
+
+ if ( con.getResponseCode() != HttpURLConnection.HTTP_OK ) {
+ return null;
+ }
+
+ is = con.getInputStream();
+ } catch ( java.net.SocketTimeoutException e ) {
+ return null;
+ } catch ( java.io.IOException e ) {
+ return null;
+ }
+ // Now read data
+ Status status;
+ try {
+ status = parseGson.fromJson( new InputStreamReader( is ), Status.class );
+ status.setAddress( address );
+ status.setTimestamp( System.currentTimeMillis() );
+ } catch ( Exception e ) {
+ e.printStackTrace();
+ status = null;
+ }
+ // TODO: http://docs.oracle.com/javase/6/docs/technotes/guides/net/http-keepalive.html
+ // once dnbd3 server supports keep-alive connections
+ return status;
+ }
+
+ public String getAddress()
+ {
+ return address;
+ }
+
+}
diff --git a/src/main/java/org/openslx/dnbd3/status/rpc/Client.java b/src/main/java/org/openslx/dnbd3/status/rpc/Client.java
new file mode 100644
index 0000000..fcb8e3e
--- /dev/null
+++ b/src/main/java/org/openslx/dnbd3/status/rpc/Client.java
@@ -0,0 +1,31 @@
+package org.openslx.dnbd3.status.rpc;
+
+public class Client
+{
+
+ private String address = null;
+ private int imageId = -1;
+ private long bytesSent = -1;
+
+ public String getAddress()
+ {
+ return address;
+ }
+
+ public int getImageId()
+ {
+ return imageId;
+ }
+
+ public long getBytesSent()
+ {
+ return bytesSent;
+ }
+
+ @Override
+ public String toString()
+ {
+ return "[Addr: " + address + ", image: " + imageId + "]";
+ }
+
+}
diff --git a/src/main/java/org/openslx/dnbd3/status/rpc/Image.java b/src/main/java/org/openslx/dnbd3/status/rpc/Image.java
new file mode 100644
index 0000000..910db82
--- /dev/null
+++ b/src/main/java/org/openslx/dnbd3/status/rpc/Image.java
@@ -0,0 +1,37 @@
+package org.openslx.dnbd3.status.rpc;
+
+public class Image
+{
+
+ private String name = null;
+ private int rid = -1;
+ private int complete = -1;
+ private int users = -1;
+
+ public String getName()
+ {
+ return name;
+ }
+
+ public int getRid()
+ {
+ return rid;
+ }
+
+ public int getComplete()
+ {
+ return complete;
+ }
+
+ public int getUsers()
+ {
+ return users;
+ }
+
+ @Override
+ public String toString()
+ {
+ return "[" + name + " (users: " + users + ")]";
+ }
+
+}
diff --git a/src/main/java/org/openslx/dnbd3/status/rpc/Status.java b/src/main/java/org/openslx/dnbd3/status/rpc/Status.java
new file mode 100644
index 0000000..07fc782
--- /dev/null
+++ b/src/main/java/org/openslx/dnbd3/status/rpc/Status.java
@@ -0,0 +1,73 @@
+package org.openslx.dnbd3.status.rpc;
+
+import java.util.List;
+
+public class Status
+{
+
+ private long bytesReceived = -1;
+ private long bytesSent = -1;
+ private int uptime = -1;
+ private List<Image> images = null;
+ private List<Client> clients = null;
+ private String address = null;
+ private long timeStamp = -1;
+
+ public long getBytesReceived()
+ {
+ return bytesReceived;
+ }
+
+ public long getBytesSent()
+ {
+ return bytesSent;
+ }
+
+ public int getUptime()
+ {
+ return uptime;
+ }
+
+ public List<Image> getImages()
+ {
+ return images;
+ }
+
+ public List<Client> getClients()
+ {
+ return clients;
+ }
+
+ public String getAddress()
+ {
+ return address;
+ }
+
+ public void setAddress( String address )
+ {
+ this.address = address;
+ }
+
+ @Override
+ public String toString()
+ {
+ String ret = "(in: " + bytesReceived + ", out: " + bytesSent;
+ if ( clients != null )
+ ret += ", clients: (" + clients.toString() + ")";
+ if ( images != null )
+ ret += ", images: (" + images.toString() + ")";
+ ret += ")";
+ return ret;
+ }
+
+ public void setTimestamp( long currentTimeMillis )
+ {
+ this.timeStamp = currentTimeMillis;
+ }
+
+ public long getTimestamp()
+ {
+ return this.timeStamp;
+ }
+
+}
diff --git a/src/main/java/org/openslx/graph/ClientNode.java b/src/main/java/org/openslx/graph/ClientNode.java
new file mode 100644
index 0000000..2d4d1f4
--- /dev/null
+++ b/src/main/java/org/openslx/graph/ClientNode.java
@@ -0,0 +1,24 @@
+package org.openslx.graph;
+
+import java.awt.Color;
+
+public class ClientNode extends Node
+{
+
+ private static final long serialVersionUID = 3569234002180936927L;
+
+ public ClientNode( String ip )
+ {
+ super( ip, ip, null );
+ setDesiredDistance( 2 );
+ setRadius( 4 );
+ setColor( Color.BLUE );
+ }
+
+ @Override
+ public int getMaxHealth()
+ {
+ return 5;
+ }
+
+}
diff --git a/src/main/java/org/openslx/graph/Edge.java b/src/main/java/org/openslx/graph/Edge.java
new file mode 100644
index 0000000..ece5250
--- /dev/null
+++ b/src/main/java/org/openslx/graph/Edge.java
@@ -0,0 +1,128 @@
+package org.openslx.graph;
+
+import java.awt.Color;
+
+import org.apache.commons.math3.util.FastMath;
+
+public class Edge implements java.io.Serializable
+{
+ private static final long serialVersionUID = 4401455477347084262L;
+ private Node _source;
+ private Node _target;
+ private double _weight;
+ private String _id = null;
+ private int _age = 0;
+ private static final Color[] COLORS = new Color[ 20 ];
+ private final int _maxHealth;
+ private int _health;
+
+ static
+ {
+ for ( int i = 0; i < COLORS.length; ++i ) {
+ int r = blend( 255, 100, i, COLORS.length - 1 );
+ int g = blend( 0, 100, i, COLORS.length - 1 );
+ int b = blend( 0, 255, i, COLORS.length - 1 );
+ COLORS[i] = new Color( r, g, b );
+ }
+ }
+
+ private static int blend( int a, int b, int current, int max )
+ {
+ a *= a;
+ b *= b;
+ return (int)FastMath.sqrt( ( ( b * current ) + ( a * ( max - current ) ) ) / max );
+ }
+
+ public Edge( Node source, Node target )
+ {
+ // Note that this graph is actually undirected.
+ _source = source;
+ _target = target;
+ _weight = 0;
+ _maxHealth = Math.max( source.getMaxHealth(), target.getMaxHealth() );
+ _health = _maxHealth;
+ }
+
+ public void resetWeight()
+ {
+ if ( _health == _maxHealth && _maxHealth != 1 && _source.wasActivated() && _target.wasActivated() ) {
+ _health = 1;
+ } else {
+ _health--;
+ }
+ _age++;
+ }
+
+ public boolean isAlive()
+ {
+ return _health > 0;
+ }
+
+ public void addWeight( double weight )
+ {
+ _weight += weight;
+ _health = _maxHealth;
+ }
+
+ public double getWeight()
+ {
+ return _weight;
+ }
+
+ public Node getSource()
+ {
+ return _source;
+ }
+
+ public Node getTarget()
+ {
+ return _target;
+ }
+
+ public boolean equals( Object o )
+ {
+ if ( o instanceof Edge ) {
+ Edge other = (Edge)o;
+ return ( _source.equals( other._source ) && _target.equals( other._target ) ) || ( _source.equals( other._target ) && _target.equals( other._source ) );
+ }
+ return false;
+ }
+
+ public boolean hasNode( Node node )
+ {
+ return _source.equals( node ) || _target.equals( node );
+ }
+
+ public double getLengthSquared()
+ {
+ final double dx = _source.getX() - _target.getX();
+ final double dy = _source.getY() - _target.getY();
+ return dx * dx + dy * dy;
+ }
+
+ public int hashCode()
+ {
+ return _source.hashCode() ^ _target.hashCode();
+ }
+
+ public String toString()
+ {
+ return _source + " " + _target + " w(" + _weight + ")";
+ }
+
+ public String getId()
+ {
+ if ( _id == null ) {
+ if ( _source.getId().compareTo( _target.getId() ) > 0 )
+ _id = _target.getId() + "==" + _source.getId();
+ _id = _source.getId() + "==" + _target.getId();
+ }
+ return _id;
+ }
+
+ public Color getColor()
+ {
+ return COLORS[_age >= COLORS.length ? COLORS.length - 1 : _age];
+ }
+
+}
diff --git a/src/main/java/org/openslx/graph/Graph.java b/src/main/java/org/openslx/graph/Graph.java
new file mode 100644
index 0000000..b3ab8b2
--- /dev/null
+++ b/src/main/java/org/openslx/graph/Graph.java
@@ -0,0 +1,559 @@
+package org.openslx.graph;
+
+import java.awt.BasicStroke;
+import java.awt.Color;
+import java.awt.Font;
+import java.awt.Graphics2D;
+import java.awt.RenderingHints;
+import java.awt.Stroke;
+import java.awt.image.BufferedImage;
+import java.io.ByteArrayOutputStream;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import org.apache.commons.math3.util.FastMath;
+
+import com.google.gson.annotations.Expose;
+
+/**
+ * The Graph stores the Nodes and Edges, and InferenceHeurisics to allow
+ * the structure of the graph to be modified.
+ */
+public class Graph implements java.io.Serializable
+{
+
+ private static final long serialVersionUID = 5488288903546706793L;
+ private String _label;
+ private String _caption = "servers are grey, clients are blue, sugar is sweet, and so are you";
+ private Map<Node, Node> _nodes = new HashMap<>();
+ private Map<Edge, Edge> _edges = new HashMap<>();
+
+ @Expose
+ private Collection<Node> nodes = _nodes.values();
+ @Expose
+ private Collection<Edge> edges = _edges.values();
+
+ private double minX = Double.POSITIVE_INFINITY;
+ private double maxX = Double.NEGATIVE_INFINITY;
+ private double minY = Double.POSITIVE_INFINITY;
+ private double maxY = Double.NEGATIVE_INFINITY;
+ private double maxWeight = 0;
+
+ private int _frameCount = 0;
+
+ private static final Color COLOR_BG = new Color( 250, 250, 255 );
+ private static final Color COLOR_TITLE = new Color( 200, 200, 255 );
+ private static final Color COLOR_LABEL = new Color( 0, 0, 20 );
+ private static final Color COLOR_LABEL_SHADOW = new Color( 220, 220, 220 );
+ private static final Color COLOR_TITLE2 = new Color( 190, 190, 210 );
+ private static final Color COLOR_EDGE = new Color( 100, 100, 255 );
+
+ private static final int IMG_X = 1280;
+ private static final int IMG_Y = 1024;
+
+ private final Graphics2D graphicsContext;
+ private final PngRenderer renderer;
+
+ public Graph( String label )
+ {
+ _label = label;
+ BufferedImage targetImage = new BufferedImage( IMG_X, IMG_Y, BufferedImage.TYPE_INT_ARGB );
+ graphicsContext = targetImage.createGraphics();
+ graphicsContext.setRenderingHint( RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON );
+ renderer = new PngRenderer( targetImage );
+ }
+
+ // Add a Node to the Graph.
+ public void addNode( Node node )
+ {
+ // Only add the Node to the HashMap if it's not already in there.
+ if ( _nodes.containsKey( node ) )
+ return;
+ _nodes.put( node, node );
+ }
+
+ // Add an Edge to the Graph. Increment the weighting if it already exists.
+ public void setEdge( Node source, Node target, double weight )
+ {
+ // Do not add self-edges or weights that are not positive.
+ if ( source.equals( target ) || weight <= 0 ) {
+ return;
+ }
+
+ addNode( source );
+ addNode( target );
+
+ // Add the Edge to the HashMap, or find the existing entry.
+ Edge edge = _edges.get( new Edge( source, target ) );
+ if ( edge == null ) {
+ source = _nodes.get( source );
+ target = _nodes.get( target );
+ edge = new Edge( source, target );
+ _edges.put( edge, edge );
+ }
+ // Set the edge weight.
+ edge.addWeight( weight );
+ }
+
+ // Remove a Node from the Graph, along with all of its emanating Edges.
+ public boolean removeNode( Node node )
+ {
+ if ( !_nodes.containsKey( node ) )
+ return false;
+ // Remove the Node from the HashMap.
+ _nodes.remove( node );
+
+ // Remove all Edges that connect to the removed Node.
+ Iterator<Edge> edgeIt = _edges.keySet().iterator();
+ while ( edgeIt.hasNext() ) {
+ Edge edge = edgeIt.next();
+ if ( edge.getSource().equals( node ) || edge.getTarget().equals( node ) ) {
+ edgeIt.remove();
+ }
+ }
+ return true;
+ }
+
+ // Return true if the Graph contains the Node.
+ // (This does not necessarily imply that the Node is visible).
+ public boolean contains( Node node )
+ {
+ return _nodes.containsKey( node );
+ }
+
+ // Return true if the Graph contains the Edge.
+ public boolean contains( Edge edge )
+ {
+ return _edges.containsKey( edge );
+ }
+
+ // Return the Graph's Node that has the same nick as the supplied Node.
+ public Node get( Node node )
+ {
+ return _nodes.get( node );
+ }
+
+ // Return the Graph's Edge that matched the supplied Edge.
+ public Edge get( Edge edge )
+ {
+ return _edges.get( edge );
+ }
+
+ public String toString()
+ {
+ return "Graph: " + _nodes.size() + " nodes and " + _edges.size() + " edges.";
+ }
+
+ public String toString2()
+ {
+ return "Nodes:\n" + _nodes + "\nEdges:\n" + _edges;
+ }
+
+ // Apply the temporal decay to the Graph.
+ public void decay()
+ {
+ for ( Iterator<Edge> it = _edges.values().iterator(); it.hasNext(); ) {
+ Edge edge = it.next();
+ edge.resetWeight();
+ if ( !edge.isAlive() ) {
+ it.remove();
+ }
+ }
+
+ List<Node> toRemove = null;
+ for ( Node node : _nodes.values() ) {
+ node.age();
+ if ( !node.isAlive() ) {
+ if ( toRemove == null ) {
+ toRemove = new ArrayList<>();
+ }
+ toRemove.add( node );
+ }
+ }
+ if ( toRemove != null ) {
+ for ( Node node : toRemove ) {
+ removeNode( node );
+ }
+ }
+ }
+
+ // Returns the set of all Nodes that have emanating Edges.
+ // This therefore returns all Nodes that will be visible in the drawing.
+ public Set<Node> getConnectedNodes()
+ {
+ Set<Node> connectedNodes = new HashSet<>();
+ for ( Edge edge : _edges.values() ) {
+ connectedNodes.add( edge.getSource() );
+ connectedNodes.add( edge.getTarget() );
+ }
+ return connectedNodes;
+ }
+
+ public Collection<Edge> getEdges()
+ {
+ return _edges.values();
+ }
+
+ // Limit movement values to stop nodes flying into oblivion.
+ private static final double MAX_MOVE = 0.75;
+ private static final double K = 2;
+ private static final double MOVEMENT_SLOWDOWN = 0.001;
+
+ // Applies the spring embedder.
+ public void doLayout( Set<Node> visibleNodes, int iterations )
+ {
+
+ // For performance, copy each set into an array.
+ Node[] nodes = visibleNodes.toArray( new Node[ visibleNodes.size() ] );
+ Edge[] edges = _edges.keySet().toArray( new Edge[ _edges.size() ] );
+
+ if ( nodes.length == 0 )
+ return;
+
+ // For each iteration...
+ for ( int it = 0; it < iterations; it++ ) {
+
+ // Calculate forces acting on nodes due to node-node repulsions...
+ for ( int a = 0; a < nodes.length; a++ ) {
+ final Node nodeA = nodes[a];
+ for ( int b = a + 1; b < nodes.length; b++ ) {
+ final Node nodeB = nodes[b];
+
+ double deltaX = nodeB.getX() - nodeA.getX();
+ double deltaY = nodeB.getY() - nodeA.getY();
+ final double maxDistance = ( nodeA.getDesiredDistance() * nodeB.getDesiredDistance() ) * 0.9;
+
+ double distanceSquared = deltaX * deltaX + deltaY * deltaY;
+
+ if ( distanceSquared > maxDistance * maxDistance )
+ continue;
+
+ if ( distanceSquared < 0.01 ) {
+ deltaX = Math.random() / 10 + 0.1;
+ deltaY = Math.random() / 10 + 0.1;
+ distanceSquared = deltaX * deltaX + deltaY * deltaY;
+ }
+
+ final double distance = FastMath.sqrt( distanceSquared );
+ final double repulsiveForce = ( K * maxDistance / distance ) * 2;
+
+ final double sumDesired = nodeA.getDesiredDistance() + nodeB.getDesiredDistance();
+ // A gets pushed away stronger if B has higher desired distance than A
+ final double rfA = repulsiveForce * ( nodeB.getDesiredDistance() / sumDesired );
+ // Vice versa for B
+ final double rfB = repulsiveForce * ( nodeA.getDesiredDistance() / sumDesired );
+
+ nodeB.addForceX( ( rfB * deltaX / distance ) );
+ nodeB.addForceY( ( rfB * deltaY / distance ) );
+ nodeA.addForceX( - ( rfA * deltaX / distance ) );
+ nodeA.addForceY( - ( rfA * deltaY / distance ) );
+ }
+
+ }
+
+ // Calculate forces acting on nodes due to edge attractions.
+ for ( int e = 0; e < edges.length; e++ ) {
+ final Edge edge = edges[e];
+ final Node nodeA = edge.getSource();
+ final Node nodeB = edge.getTarget();
+
+ final double minDistance = ( nodeA.getDesiredDistance() * nodeB.getDesiredDistance() ) * 1.1;
+
+ final double deltaX = nodeB.getX() - nodeA.getX();
+ final double deltaY = nodeB.getY() - nodeA.getY();
+
+ double distanceSquared = deltaX * deltaX + deltaY * deltaY;
+
+ if ( distanceSquared < minDistance * minDistance )
+ continue;
+
+ double distance = FastMath.sqrt( distanceSquared );
+
+ if ( distance > minDistance * 2 ) {
+ distance = minDistance * 2;
+ distanceSquared = distance * distance;
+ }
+
+ final double attractiveForce = ( distanceSquared / K ) * 2;
+ final double sumDesired = nodeA.getDesiredDistance() + nodeB.getDesiredDistance();
+ // A gets pulled towards B stronger if B has higher desired distance than A
+ final double afA = attractiveForce * ( nodeB.getDesiredDistance() / sumDesired );
+ // Vice versa for B
+ final double afB = attractiveForce * ( nodeA.getDesiredDistance() / sumDesired );
+
+ nodeB.addForceX( -afB * deltaX / distance );
+ nodeB.addForceY( -afB * deltaY / distance );
+ nodeA.addForceX( afA * deltaX / distance );
+ nodeA.addForceY( afA * deltaY / distance );
+
+ }
+
+ // Now move each node to its new location...
+ for ( int a = 0; a < nodes.length; a++ ) {
+ Node node = nodes[a];
+
+ // General weak attraction towards (0|0)
+ if ( node.getDesiredDistance() > 5 ) {
+ node.addForceX( -node.getX() / 500 );
+ node.addForceY( -node.getY() / 500 );
+ }
+
+ double xMovement = MOVEMENT_SLOWDOWN * node.getFX();
+ double yMovement = MOVEMENT_SLOWDOWN * node.getFY();
+
+ if ( xMovement > MAX_MOVE ) {
+ xMovement = MAX_MOVE;
+ }
+ else if ( xMovement < -MAX_MOVE ) {
+ xMovement = -MAX_MOVE;
+ }
+ if ( yMovement > MAX_MOVE ) {
+ yMovement = MAX_MOVE;
+ }
+ else if ( yMovement < -MAX_MOVE ) {
+ yMovement = -MAX_MOVE;
+ }
+
+ node.setX( node.getX() + xMovement );
+ node.setY( node.getY() + yMovement );
+
+ // Reset the forces
+ node.resetForce();
+ }
+
+ // Swap two random nodes if it results in less stress on them
+ Node a = nodes[(int) ( Math.random() * nodes.length )];
+ Node b = nodes[(int) ( Math.random() * nodes.length )];
+ if ( !a.equals( b ) ) {
+ double initialLen = getEdgeLenSum( edges, a, b );
+ a.swapPos( b );
+ double swappedLen = getEdgeLenSum( edges, a, b );
+ if ( swappedLen + 0.5 > initialLen ) {
+ a.swapPos( b ); // Isn't any better, swap back
+ }
+ }
+
+ }
+
+ }
+
+ private double getEdgeLenSum( Edge[] edges, Node a, Node b )
+ {
+ double result = 0;
+ for ( Edge edge : edges ) {
+ if ( edge.hasNode( a ) || edge.hasNode( b ) ) {
+ result += edge.getLengthSquared();
+ }
+ }
+ return result;
+ }
+
+ // Work out the drawing boundaries...
+ public void calcBounds( Set<Node> nodes )
+ {
+
+ minX = Double.POSITIVE_INFINITY;
+ maxX = Double.NEGATIVE_INFINITY;
+ minY = Double.POSITIVE_INFINITY;
+ maxY = Double.NEGATIVE_INFINITY;
+ maxWeight = 0;
+
+ for ( Node node : nodes ) {
+
+ if ( node.getX() > maxX ) {
+ maxX = node.getX();
+ }
+ if ( node.getX() < minX ) {
+ minX = node.getX();
+ }
+ if ( node.getY() > maxY ) {
+ maxY = node.getY();
+ }
+ if ( node.getY() < minY ) {
+ minY = node.getY();
+ }
+ }
+
+ // Increase size if too small.
+ final double minSize = 10;
+ if ( maxX - minX < minSize ) {
+ double midX = ( maxX + minX ) / 2;
+ minX = midX - ( minSize / 2 );
+ maxX = midX + ( minSize / 2 );
+ }
+ if ( maxY - minY < minSize ) {
+ double midY = ( maxY + minY ) / 2;
+ minY = midY - ( minSize / 2 );
+ maxY = midY + ( minSize / 2 );
+ }
+
+ // Work out the maximum weight.
+ for ( Edge edge : _edges.values() ) {
+ if ( edge.getWeight() > maxWeight ) {
+ maxWeight = edge.getWeight();
+ }
+ }
+
+ // Jibble the boundaries to maintain the aspect ratio.
+ double xyRatio = ( ( maxX - minX ) / ( maxY - minY ) ) / ( IMG_X / IMG_Y );
+ if ( xyRatio > 1 ) {
+ // diagram is wider than it is high.
+ double dy = maxY - minY;
+ dy = dy * xyRatio - dy;
+ minY = minY - dy / 2;
+ maxY = maxY + dy / 2;
+ }
+ else if ( xyRatio < 1 ) {
+ // Diagram is higher than it is wide.
+ double dx = maxX - minX;
+ dx = dx / xyRatio - dx;
+ minX = minX - dx / 2;
+ maxX = maxX + dx / 2;
+ }
+
+ }
+
+ private static final Font FONT_64 = new Font( "SansSerif", Font.BOLD, 64 );
+ private static final Font FONT_18 = new Font( "SansSerif", Font.BOLD, 18 );
+ private static final Font FONT_12 = new Font( "SansSerif", Font.PLAIN, 12 );
+ private static final Font FONT_10 = new Font( "SansSerif", Font.PLAIN, 10 );
+ private static final Stroke STROKE_2 = new BasicStroke( 2.0f );
+
+ public void drawImage( Set<Node> nodes, double edgeThreshold )
+ {
+
+ // Now actually draw the thing...
+
+ graphicsContext.setColor( COLOR_BG );
+ graphicsContext.fillRect( 0, 0, IMG_X, IMG_Y );
+
+ graphicsContext.setColor( COLOR_TITLE );
+ graphicsContext.setFont( FONT_64 );
+ graphicsContext.drawString( _label, 20, 80 );
+
+ graphicsContext.setColor( COLOR_TITLE2 );
+ graphicsContext.setFont( FONT_18 );
+ graphicsContext.drawString( _caption, 0, IMG_Y - 5 - 50 );
+ graphicsContext.setFont( FONT_12 );
+ graphicsContext.drawString( "This frame was drawn at " + new Date(), 0, IMG_Y - 5 );
+
+ // Draw all edges...
+ for ( Edge edge : _edges.values() ) {
+ if ( edge.getWeight() < edgeThreshold ) {
+ continue;
+ }
+
+ double weight = edge.getWeight();
+
+ Node nodeA = edge.getSource();
+ Node nodeB = edge.getTarget();
+ int x1 = toImageX( nodeA );
+ int y1 = toImageY( nodeA );
+ int x2 = toImageX( nodeB );
+ int y2 = toImageY( nodeB );
+ graphicsContext.setStroke( new BasicStroke( (float) ( FastMath.log( weight + 1 ) * 0.1 ) + 1 ) );
+ final int alpha = 102 + (int) ( 153 * weight / maxWeight );
+ graphicsContext.setColor( new Color( ( edge.getColor().getRGB() & 0xffffff ) | ( alpha << 24 ), true ) );
+ graphicsContext.drawLine( x1, y1, x2, y2 );
+ }
+
+ // Draw all nodes...
+ graphicsContext.setStroke( STROKE_2 );
+ graphicsContext.setFont( FONT_10 );
+ for ( Node node : nodes ) {
+ final int x = toImageX( node );
+ final int y = toImageY( node );
+ final int nodeRadius = node.getRadius();
+ graphicsContext.setColor( node.getColor() );
+ graphicsContext.fillOval( x - nodeRadius, y - nodeRadius, nodeRadius * 2, nodeRadius * 2 );
+ graphicsContext.setColor( COLOR_EDGE );
+ graphicsContext.drawOval( x - nodeRadius, y - nodeRadius, nodeRadius * 2, nodeRadius * 2 );
+ graphicsContext.setColor( COLOR_LABEL_SHADOW );
+ graphicsContext.drawString( node.toString(), x - ( node.toString().length() * 3 ) + 1, y - nodeRadius - 2 + 1 );
+ graphicsContext.setColor( COLOR_LABEL );
+ graphicsContext.drawString( node.toString(), x - ( node.toString().length() * 3 ), y - nodeRadius - 2 );
+ }
+ }
+
+ private int toImageX( Node node )
+ {
+ return (int) ( ( IMG_X - 50 ) * ( node.getX() - minX ) / ( maxX - minX ) ) + 25;
+ }
+
+ private int toImageY( Node node )
+ {
+ return (int) ( ( IMG_Y - 55 ) * ( node.getY() - minY ) / ( maxY - minY ) ) + 35;
+ }
+
+ public int getFrameCount()
+ {
+ return _frameCount;
+ }
+
+ public String getLabel()
+ {
+ return _label;
+ }
+
+ public void setCaption( String caption )
+ {
+ _caption = caption;
+ }
+
+ public byte[] makeNextImage()
+ {
+ _frameCount++;
+
+ Set<Node> nodes = getConnectedNodes();
+
+ doLayout( nodes, 200 );
+ calcBounds( nodes );
+
+ ByteArrayOutputStream baos = new ByteArrayOutputStream( 100000 );
+
+ try {
+ drawImage( nodes, 0.1 );
+ //ImageIO.write( targetImage, "png", baos );
+ renderer.render( baos );
+
+ writeGraph();
+ return baos.toByteArray();
+ } catch ( Exception e ) {
+ System.out.println( "PieSpy has gone wibbly: " + e );
+ e.printStackTrace();
+ }
+ return null;
+ }
+
+ // Serialize this Graph and write it to a File.
+ public void writeGraph()
+ {
+ /*
+ try {
+ String strippedChannel = _label.toLowerCase().substring( 1 );
+ File dir = new File( config.outputDirectory, strippedChannel );
+ File file = new File( dir, strippedChannel + "-restore.dat" );
+ ObjectOutputStream oos = new ObjectOutputStream( new FileOutputStream( file ) );
+ oos.writeObject( SocialNetworkBot.VERSION );
+ oos.writeObject( this );
+ oos.flush();
+ oos.close();
+ } catch ( Exception e ) {
+ // Do nothing?
+ }
+ */
+ }
+
+ public Node getNode( String id )
+ {
+ return _nodes.get( new Node( id ) );
+ }
+
+}
diff --git a/src/main/java/org/openslx/graph/Node.java b/src/main/java/org/openslx/graph/Node.java
new file mode 100644
index 0000000..0bf2fab
--- /dev/null
+++ b/src/main/java/org/openslx/graph/Node.java
@@ -0,0 +1,198 @@
+package org.openslx.graph;
+
+import java.awt.Color;
+
+import com.google.gson.annotations.Expose;
+import com.google.gson.annotations.SerializedName;
+
+public class Node implements java.io.Serializable
+{
+ private static final long serialVersionUID = -6997045538537830300L;
+
+ private final String _id;
+ @Expose
+ @SerializedName( "name" )
+ private final String _displayName;
+ @Expose
+ @SerializedName( "distance" )
+ private double _distance = 2;
+ @Expose
+ @SerializedName( "radius" )
+ private int _radius = 2;
+ @Expose
+ @SerializedName( "color" )
+ private String _colorString;
+ @Expose
+ @SerializedName( "x" )
+ private double _x;
+ @Expose
+ @SerializedName( "y" )
+ private double _y;
+ private double _fx;
+ private double _fy;
+ private int _health;
+ private Color _color;
+
+ protected Node( String displayName )
+ {
+ this( displayName, displayName, null );
+ }
+
+ protected Node( String id, String displayName, Node startPos )
+ {
+ _displayName = displayName;
+ _id = id;
+ if ( startPos == null ) {
+ _x = Math.random() * 2;
+ _y = Math.random() * 2;
+ } else {
+ _x = startPos.getX();
+ _y = startPos.getY();
+ }
+ _fx = 0;
+ _fy = 0;
+ _health = getMaxHealth();
+ setColor( Color.BLUE );
+ }
+
+ public void swapPos( Node other )
+ {
+ final double tx = _x, ty = _y;
+ _x = other._x;
+ _y = other._y;
+ other._x = tx;
+ other._y = ty;
+ }
+
+ public void setX( double x )
+ {
+ _x = x;
+ }
+
+ public void setY( double y )
+ {
+ _y = y;
+ }
+
+ public void addForceX( double fx )
+ {
+ _fx += fx;
+ }
+
+ public void addForceY( double fy )
+ {
+ _fy += fy;
+ }
+
+ public void resetForce()
+ {
+ _fx = 0;
+ _fy = 0;
+ }
+
+ public double getX()
+ {
+ return _x;
+ }
+
+ public double getY()
+ {
+ return _y;
+ }
+
+ public double getFX()
+ {
+ return _fx;
+ }
+
+ public double getFY()
+ {
+ return _fy;
+ }
+
+ public String toString()
+ {
+ return _displayName;
+ }
+
+ public String getId()
+ {
+ return _id;
+ }
+
+ public final boolean equals( Object o )
+ {
+ if ( o instanceof Node ) {
+ Node other = (Node)o;
+ return _id.equals( other._id );
+ }
+ return false;
+ }
+
+ public final int hashCode()
+ {
+ return _id.hashCode();
+ }
+
+ public final void activate()
+ {
+ _health = getMaxHealth();
+ }
+
+ public final boolean isAlive()
+ {
+ return _health > 0;
+ }
+
+ public final void age()
+ {
+ _health--;
+ }
+
+ public final Color getColor()
+ {
+ return _color;
+ }
+
+ protected final void setColor( Color color )
+ {
+ _color = color;
+ _colorString = String.format( "#%02X%02X%02X", color.getRed(), color.getGreen(), color.getBlue() );
+ }
+
+ public final int getRadius()
+ {
+ return 5;
+ }
+
+ protected final void setRadius( int radius )
+ {
+ _radius = radius;
+ }
+
+ public int getMaxHealth()
+ {
+ return 1;
+ }
+
+ public final boolean wasActivated()
+ {
+ return _health == getMaxHealth();
+ }
+
+ public final double getDesiredDistance()
+ {
+ return _distance;
+ }
+
+ protected final void setDesiredDistance( double distance )
+ {
+ _distance = distance;
+ }
+
+ public final int getAlpha()
+ {
+ return 63 + ( _health * 192 ) / getMaxHealth();
+ }
+
+}
diff --git a/src/main/java/org/openslx/graph/PngRenderer.java b/src/main/java/org/openslx/graph/PngRenderer.java
new file mode 100644
index 0000000..e51889c
--- /dev/null
+++ b/src/main/java/org/openslx/graph/PngRenderer.java
@@ -0,0 +1,46 @@
+package org.openslx.graph;
+
+import java.awt.image.BufferedImage;
+import java.awt.image.DataBufferInt;
+import java.awt.image.SinglePixelPackedSampleModel;
+import java.io.OutputStream;
+
+import ar.com.hjg.pngj.ImageInfo;
+import ar.com.hjg.pngj.ImageLineByte;
+import ar.com.hjg.pngj.PngWriter;
+
+public class PngRenderer
+{
+ private final ImageInfo imageInfo;
+ private final SinglePixelPackedSampleModel samplemodel;
+ private final int[] srcImageRawData;
+ private final ImageLineByte line;
+ private final byte[] lineBytes;
+
+ public PngRenderer( BufferedImage image )
+ {
+ imageInfo = new ImageInfo( image.getWidth(), image.getHeight(), 8, false );
+ DataBufferInt db = ( (DataBufferInt)image.getRaster().getDataBuffer() );
+ samplemodel = (SinglePixelPackedSampleModel)image.getSampleModel();
+ line = new ImageLineByte( imageInfo );
+ lineBytes = line.getScanline();
+ srcImageRawData = db.getData();
+ }
+
+ public void render( OutputStream os )
+ {
+ PngWriter pngWriter = new PngWriter( os, imageInfo );
+ for ( int row = 0; row < imageInfo.rows; row++ ) {
+ int elem = samplemodel.getOffset( 0, row );
+ for ( int col = 0, j = 0; col < imageInfo.cols; col++ ) {
+ int sample = srcImageRawData[elem++];
+ lineBytes[j++] = (byte) ( ( sample & 0xFF0000 ) >> 16 ); // R
+ lineBytes[j++] = (byte) ( ( sample & 0xFF00 ) >> 8 ); // G
+ lineBytes[j++] = (byte) ( sample & 0xFF ); // B
+ }
+ pngWriter.writeRow( line, row );
+ }
+ pngWriter.end();
+ }
+
+}
diff --git a/src/main/java/org/openslx/graph/ServerNode.java b/src/main/java/org/openslx/graph/ServerNode.java
new file mode 100644
index 0000000..c7604fc
--- /dev/null
+++ b/src/main/java/org/openslx/graph/ServerNode.java
@@ -0,0 +1,23 @@
+package org.openslx.graph;
+
+import java.awt.Color;
+
+public class ServerNode extends Node
+{
+ private static final long serialVersionUID = 1642125227049150051L;
+
+ public ServerNode( String ip )
+ {
+ super( ip, ip, null );
+ setDesiredDistance( 6 );
+ setRadius( 15 );
+ setColor( Color.LIGHT_GRAY );
+ }
+
+ @Override
+ public int getMaxHealth()
+ {
+ return 15;
+ }
+
+}
diff --git a/static/index.html b/static/index.html
new file mode 100644
index 0000000..60053af
--- /dev/null
+++ b/static/index.html
@@ -0,0 +1,7 @@
+<html><body style="padding:0;margin:0;overflow:hidden"><img alt="" src="image.png" id="sasa" style="padding:0;margin:0">
+<script type="text/javascript">
+setInterval(function() {
+ document.getElementById("sasa").src = '/image.png?a=' + Math.random();
+}, 2000);
+</script>
+</body></html>