From 45ada5475192f6afc9645c401de46765be87ee3f Mon Sep 17 00:00:00 2001 From: Simon Rettberg Date: Thu, 21 May 2015 16:39:25 +0200 Subject: Lean and mean first prototype - to be improved! --- .gitignore | 9 + pom.xml | 69 ++ src/main/java/fi/iki/elonen/NanoHTTPD.java | 1098 ++++++++++++++++++++ src/main/java/org/openslx/dnbd3/status/App.java | 14 + .../openslx/dnbd3/status/StatisticsGenerator.java | 146 +++ .../java/org/openslx/dnbd3/status/WebServer.java | 100 ++ .../dnbd3/status/output/EdgeSerializer.java | 25 + .../openslx/dnbd3/status/output/OutputMain.java | 19 + .../openslx/dnbd3/status/output/ServerStats.java | 19 + .../openslx/dnbd3/status/poller/ServerPoller.java | 70 ++ .../java/org/openslx/dnbd3/status/rpc/Client.java | 31 + .../java/org/openslx/dnbd3/status/rpc/Image.java | 37 + .../java/org/openslx/dnbd3/status/rpc/Status.java | 73 ++ src/main/java/org/openslx/graph/ClientNode.java | 24 + src/main/java/org/openslx/graph/Edge.java | 128 +++ src/main/java/org/openslx/graph/Graph.java | 559 ++++++++++ src/main/java/org/openslx/graph/Node.java | 198 ++++ src/main/java/org/openslx/graph/PngRenderer.java | 46 + src/main/java/org/openslx/graph/ServerNode.java | 23 + static/index.html | 7 + 20 files changed, 2695 insertions(+) create mode 100644 .gitignore create mode 100644 pom.xml create mode 100644 src/main/java/fi/iki/elonen/NanoHTTPD.java create mode 100644 src/main/java/org/openslx/dnbd3/status/App.java create mode 100644 src/main/java/org/openslx/dnbd3/status/StatisticsGenerator.java create mode 100644 src/main/java/org/openslx/dnbd3/status/WebServer.java create mode 100644 src/main/java/org/openslx/dnbd3/status/output/EdgeSerializer.java create mode 100644 src/main/java/org/openslx/dnbd3/status/output/OutputMain.java create mode 100644 src/main/java/org/openslx/dnbd3/status/output/ServerStats.java create mode 100644 src/main/java/org/openslx/dnbd3/status/poller/ServerPoller.java create mode 100644 src/main/java/org/openslx/dnbd3/status/rpc/Client.java create mode 100644 src/main/java/org/openslx/dnbd3/status/rpc/Image.java create mode 100644 src/main/java/org/openslx/dnbd3/status/rpc/Status.java create mode 100644 src/main/java/org/openslx/graph/ClientNode.java create mode 100644 src/main/java/org/openslx/graph/Edge.java create mode 100644 src/main/java/org/openslx/graph/Graph.java create mode 100644 src/main/java/org/openslx/graph/Node.java create mode 100644 src/main/java/org/openslx/graph/PngRenderer.java create mode 100644 src/main/java/org/openslx/graph/ServerNode.java create mode 100644 static/index.html 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 @@ + + 4.0.0 + org.openslx + dnbd3-status + jar + 0.0.1-SNAPSHOT + DNBD3 Status Monitor + + + UTF-8 + true + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.1 + + 1.7 + 1.7 + true + + + + maven-assembly-plugin + + + package + + single + + + + + + + org.openslx.dnbd3.status.App + + + + + jar-with-dependencies + + + + + + + + com.google.code.gson + gson + 2.2.4 + compile + + + org.apache.commons + commons-math3 + 3.5 + + + ar.com.hjg + pngj + 2.1.0 + + + \ 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 + *

+ *

+ * NanoHTTPD + *

+ * Copyright (c) 2012-2013 by Paul S. Hawke, 2001,2005-2013 by Jarno Elonen, 2010 by Konstantinos + * Togias + *

+ *

+ *

+ * Features + limitations: + *

+ *

+ *

+ * How to use: + *

+ *

+ * 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 openConnections = new HashSet(); + /** + * 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. + *

+ *

+ * (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 headers, Map parms, + Map files ) + { + return new Response( Response.Status.NOT_FOUND, MIME_PLAINTEXT, "Not Found" ); + } + + /** + * Override this to customize the server. + *

+ *

+ * (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 files = new HashMap(); + 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 parms = session.getParms(); + parms.put( QUERY_STRING_PARAMETER, session.getQueryParameterString() ); + return serve( session.getUri(), method, session.getHeaders(), parms, files ); + } + + /** + * Decode percent encoded String values. + * + * @param str the percent encoded String + * @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 NanoHTTPD parameters values, as passed to the + * serve() method. + * @return a map of String (parameter name) to List<String> (a + * list of the values supplied). + */ + protected Map> decodeParameters( Map 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 String (parameter name) to List<String> (a + * list of the values supplied). + */ + protected Map> decodeParameters( String queryString ) + { + Map> parms = new HashMap>(); + 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 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 String 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. + *

+ *

+ * By default, the server spawns a new Thread for every incoming request. These are set to + * daemon status, and named according to the request number. The name is useful when + * profiling the application. + *

+ */ + public static class DefaultAsyncRunner implements AsyncRunner + { + private long requestCount; + private ExecutorService pool = new ThreadPoolExecutor( 4, 32, 1, TimeUnit.MINUTES, new ArrayBlockingQueue( 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 header = new HashMap(); + /** + * 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 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 header ) + { + if ( !headerAlreadySent( header, "connection" ) ) { + pw.print( "Connection: keep-alive\r\n" ); + } + } + + private boolean headerAlreadySent( Map 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 getParms(); + + Map 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 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 parms; + private Map 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(); + } + + @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(); + if ( null == headers ) { + headers = new HashMap(); + } 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 pre = new HashMap(); + 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 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 pre, Map parms, Map 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 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 getParms() + { + return parms; + } + + public String getQueryParameterString() + { + return queryParameterString; + } + + @Override + public final Map 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 pollers; + private volatile long lastUpdate = 0; + private ExecutorService threadPool = new ThreadPoolExecutor( 1, 6, 1, TimeUnit.MINUTES, new ArrayBlockingQueue( 100 ) ); + private List> futureStatusList = new ArrayList<>(); + private List 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 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 ret = threadPool.submit( new Callable() { + @Override + public Status call() throws Exception + { + return p.update(); + } + } ); + futureStatusList.add( ret ); + } + statusList.clear(); + output.servers.clear(); + for ( Future 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 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 +{ + + @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 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 images = null; + private List 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 getImages() + { + return images; + } + + public List 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 _nodes = new HashMap<>(); + private Map _edges = new HashMap<>(); + + @Expose + private Collection nodes = _nodes.values(); + @Expose + private Collection 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 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 it = _edges.values().iterator(); it.hasNext(); ) { + Edge edge = it.next(); + edge.resetWeight(); + if ( !edge.isAlive() ) { + it.remove(); + } + } + + List 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 getConnectedNodes() + { + Set connectedNodes = new HashSet<>(); + for ( Edge edge : _edges.values() ) { + connectedNodes.add( edge.getSource() ); + connectedNodes.add( edge.getTarget() ); + } + return connectedNodes; + } + + public Collection 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 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 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 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 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 @@ + + + -- cgit v1.2.3-55-g7522