httpHeaders) {
String raw = httpHeaders.get("cookie");
if (raw != null) {
String[] tokens = raw.split(";");
for (String token : tokens) {
String[] data = token.trim().split("=");
if (data.length == 2) {
this.cookies.put(data[0], data[1]);
}
}
}
}
/**
* Set a cookie with an expiration date from a month ago, effectively
* deleting it on the client side.
*
* @param name
* The cookie name.
*/
public void delete(String name) {
set(name, "-delete-", -30);
}
@Override
public Iterator iterator() {
return this.cookies.keySet().iterator();
}
/**
* Read a cookie from the HTTP Headers.
*
* @param name
* The cookie's name.
* @return The cookie's value if it exists, null otherwise.
*/
public String read(String name) {
return this.cookies.get(name);
}
public void set(Cookie cookie) {
this.queue.add(cookie);
//cookies.put(cookie.n, cookie.v); // CHANGED
}
/**
* Sets a cookie.
*
* @param name
* The cookie's name.
* @param value
* The cookie's value.
* @param expires
* How many days until the cookie expires.
*/
public Cookie set(String name, String value, int expires) {
return addAndReturn(this.queue, new Cookie(name, value, Cookie.getHTTPTime(expires)));
}
/**
* Internally used by the webserver to add all queued cookies into the
* Response's HTTP Headers.
*
* @param response
* The Response object to which headers the queued cookies
* will be added.
*/
public void unloadQueue(Response response) {
for (Cookie cookie : this.queue) {
response.addHeader("Set-Cookie", cookie.getHTTPHeader());
}
}
}
/**
* 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 final List running = Collections.synchronizedList(new ArrayList());
/**
* @return a list with currently running clients.
*/
public List getRunning() {
return running;
}
@Override
public void closeAll() {
// copy of the list for concurrency
for (ClientHandler clientHandler : new ArrayList(this.running)) {
clientHandler.close();
}
}
@Override
public void closed(ClientHandler clientHandler) {
this.running.remove(clientHandler);
}
@Override
public void exec(ClientHandler clientHandler) {
++this.requestCount;
Thread t = new Thread(clientHandler);
//t.setDaemon(true);
String clientIP = "?";
try { clientIP = clientHandler.acceptSocket.getInetAddress().getHostAddress().toString(); } catch (Throwable __e) { printStackTrace(__e); }
t.setName("NanoHttpd serving request #" + this.requestCount + " to " + clientIP);
this.running.add(clientHandler);
t.start();
}
}
/**
* Default strategy for creating and cleaning up temporary files.
*
*
* By default, files are created by File.createTempFile() in
* the directory specified.
*
*/
public static class DefaultTempFile implements TempFile {
private final File file;
private final OutputStream fstream;
public DefaultTempFile(String tempdir) throws IOException {
this.file = File.createTempFile("NanoHTTPD-", "", new File(tempdir));
this.fstream = new FileOutputStream(this.file);
System.err.println("Temp file created: " + file);
}
@Override
public void delete() throws Exception {
safeClose(this.fstream);
System.err.println("Temp file deleted: " + file);
if (!this.file.delete()) {
throw new Exception("could not delete temporary file");
}
}
@Override
public String getName() {
return this.file.getAbsolutePath();
}
@Override
public OutputStream open() throws Exception {
return this.fstream;
}
}
/**
* Default strategy for creating and cleaning up temporary files.
*
*
* This class stores its files in the standard location (that is, wherever
* java.io.tmpdir points to). Files are added to an internal
* list, and deleted when no longer needed (that is, when
* clear() is invoked at the end of processing a request).
*
*/
public static class DefaultTempFileManager implements TempFileManager {
private final String tmpdir;
private final List tempFiles;
public DefaultTempFileManager() {
this.tmpdir = tempDir().getPath(); // use JavaX, dude
// System.getProperty("java.io.tmpdir");
this.tempFiles = new ArrayList();
}
@Override
public void clear() {
for (TempFile file : this.tempFiles) {
try {
file.delete();
} catch (Exception ignored) {
NanoHTTPD.LOG.log(Level.WARNING, "could not delete file ", ignored);
}
}
this.tempFiles.clear();
}
@Override
public TempFile createTempFile() throws Exception {
DefaultTempFile tempFile = new DefaultTempFile(this.tmpdir);
this.tempFiles.add(tempFile);
return tempFile;
}
}
/**
* Default strategy for creating and cleaning up temporary files.
*/
private class DefaultTempFileManagerFactory implements TempFileManagerFactory {
@Override
public TempFileManager create() {
return new DefaultTempFileManager();
}
}
private static final String CONTENT_DISPOSITION_REGEX = "([ |\t]*Content-Disposition[ |\t]*:)(.*)";
private static final Pattern CONTENT_DISPOSITION_PATTERN = Pattern.compile(CONTENT_DISPOSITION_REGEX, Pattern.CASE_INSENSITIVE);
private static final String CONTENT_TYPE_REGEX = "([ |\t]*content-type[ |\t]*:)(.*)";
private static final Pattern CONTENT_TYPE_PATTERN = Pattern.compile(CONTENT_TYPE_REGEX, Pattern.CASE_INSENSITIVE);
private static final String CONTENT_DISPOSITION_ATTRIBUTE_REGEX = "[ |\t]*([a-zA-Z]*)[ |\t]*=[ |\t]*['|\"]([^\"^']*)['|\"]";
private static final Pattern CONTENT_DISPOSITION_ATTRIBUTE_PATTERN = Pattern.compile(CONTENT_DISPOSITION_ATTRIBUTE_REGEX);
class HTTPSession implements IHTTPSession {
boolean badClient = false;
boolean specialHandler = false; // we handle the request outside of NanoHTTPD
long opened = sysNow();
public static final int BUFSIZE = 8192;
private final TempFileManager tempFileManager;
private Socket socket;
private final OutputStream outputStream;
private final PushbackInputStream inputStream;
private int splitbyte;
private int rlen;
private String uri;
private Method method;
private Map parms;
private Map headers;
Map files = new HashMap();
private CookieHandler cookies;
private String queryParameterString;
private String remoteIp;
private String protocolVersion;
private String fullURI;
List headerLines = collectHeaderLines ? new ArrayList() : null;
public HTTPSession(TempFileManager tempFileManager,
Socket socket, InputStream inputStream, OutputStream outputStream) {
this.socket = socket;
this.tempFileManager = tempFileManager;
this.inputStream = new PushbackInputStream(inputStream, HTTPSession.BUFSIZE);
this.outputStream = outputStream;
}
public String getProtocolVersion() { return protocolVersion; }
public String getFullURI() { return fullURI; }
public List getHeaderLines() { return headerLines; }
public HTTPSession(TempFileManager tempFileManager, Socket socket, InputStream inputStream, OutputStream outputStream, InetAddress inetAddress) {
this.socket = socket;
this.tempFileManager = tempFileManager;
this.inputStream = new PushbackInputStream(inputStream, HTTPSession.BUFSIZE);
this.outputStream = outputStream;
this.remoteIp = inetAddress.isLoopbackAddress() || inetAddress.isAnyLocalAddress() ? "127.0.0.1" : inetAddress.getHostAddress().toString();
this.headers = new LinkedHashMap();
}
public void badClient(boolean b) { badClient = b; }
public void specialHandler(boolean b) { specialHandler = b; }
/**
* Decodes the sent headers and loads the data into Key/value pairs
*/
private void decodeHeader(BufferedReader in, Map pre, Map parms, Map headers, Map files) 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(Status.BAD_REQUEST, "BAD REQUEST: Syntax error. Usage: GET /example/file.html");
}
pre.put("method", st.nextToken());
if (!st.hasMoreTokens()) {
throw new ResponseException(Status.BAD_REQUEST, "BAD REQUEST: Missing URI. Usage: GET /example/file.html");
}
String uri = st.nextToken();
fullURI = uri;
// Decode parameters from the URI
int qmi = uri.indexOf('?');
if (qmi >= 0) {
String query = uri.substring(qmi + 1);
files.put("query", query);
decodeParms(query, parms);
uri = uri.substring(0, qmi);
}
if (decodePercentInURI)
uri = decodePercent(uri);
// If there's another token, its protocol version,
// followed by HTTP headers.
// NOTE: this now forces header names lower case since they are
// case insensitive and vary by client.
if (st.hasMoreTokens()) {
protocolVersion = st.nextToken();
} else {
protocolVersion = "HTTP/1.1";
NanoHTTPD.LOG.log(Level.FINE, "no protocol version specified, strange. Assuming HTTP/1.1.");
}
String line = in.readLine();
while (line != null && line.trim().length() > 0) {
{ if (headerLines != null) headerLines.add(line); }
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(Status.INTERNAL_ERROR, "SERVER INTERNAL ERROR: IOException: " + ioe.getMessage(), ioe);
}
}
/**
* Decodes the Multipart Body data and put it into Key/Value pairs.
*/
private void decodeMultipartFormData(String boundary, java.nio.ByteBuffer fbuf, Map parms, Map files) throws ResponseException {
try {
int[] boundary_idxs = getBoundaryPositions(fbuf, boundary.getBytes());
if (boundary_idxs.length < 2) {
throw new ResponseException(
Status.BAD_REQUEST, "BAD REQUEST: Content type is multipart/form-data but contains less than two boundary strings.");
}
final int MAX_HEADER_SIZE = 1024;
byte[] part_header_buff = new byte[MAX_HEADER_SIZE];
for (int bi = 0; bi < boundary_idxs.length - 1; bi++) {
fbuf.position(boundary_idxs[bi]);
int len = (fbuf.remaining() < MAX_HEADER_SIZE) ? fbuf.remaining() : MAX_HEADER_SIZE;
fbuf.get(part_header_buff, 0, len);
ByteArrayInputStream bais = new ByteArrayInputStream(part_header_buff, 0, len);
BufferedReader in = new BufferedReader(new InputStreamReader(bais, Charset.forName("US-ASCII")));
// First line is boundary string
String mpline = in.readLine();
if (!mpline.contains(boundary)) {
throw new ResponseException(Status.BAD_REQUEST, "BAD REQUEST: Content type is multipart/form-data but chunk does not start with boundary.");
}
String part_name = null, file_name = null, content_type = null;
// Parse the reset of the header lines
mpline = in.readLine();
while (mpline != null && mpline.trim().length() > 0) {
Matcher matcher = CONTENT_DISPOSITION_PATTERN.matcher(mpline);
if (matcher.matches()) {
String attributeString = matcher.group(2);
matcher = CONTENT_DISPOSITION_ATTRIBUTE_PATTERN.matcher(attributeString);
while (matcher.find()) {
String key = matcher.group(1);
if (key.equalsIgnoreCase("name")) {
part_name = matcher.group(2);
} else if (key.equalsIgnoreCase("filename")) {
file_name = matcher.group(2);
}
}
}
matcher = CONTENT_TYPE_PATTERN.matcher(mpline);
if (matcher.matches()) {
content_type = matcher.group(2).trim();
}
mpline = in.readLine();
}
// Read the part data
int part_header_len = len - (int) in.skip(MAX_HEADER_SIZE);
if (part_header_len >= len - 4) {
throw new ResponseException(Status.INTERNAL_ERROR, "Multipart header size exceeds MAX_HEADER_SIZE.");
}
int part_data_start = boundary_idxs[bi] + part_header_len;
int part_data_end = boundary_idxs[bi + 1] - 4;
fbuf.position(part_data_start);
if (content_type == null) {
// Read the part into a string
byte[] data_bytes = new byte[part_data_end - part_data_start];
fbuf.get(data_bytes);
parms.put(part_name, new String(data_bytes));
} else {
// Read it into a file
String path = saveTmpFile(fbuf, part_data_start, part_data_end - part_data_start);
if (!files.containsKey(part_name)) {
files.put(part_name, path);
} else {
int count = 2;
while (files.containsKey(part_name + count)) {
count++;
}
files.put(part_name + count, path);
}
parms.put(part_name, file_name);
}
}
} catch (ResponseException re) {
throw re;
} catch (Exception e) {
throw new ResponseException(Status.INTERNAL_ERROR, e.toString());
}
}
/**
* 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) {
this.queryParameterString = "";
return;
}
this.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 void execute() throws IOException {
Response r = null;
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[HTTPSession.BUFSIZE];
this.splitbyte = 0;
this.rlen = 0;
int read = -1;
try {
read = this.inputStream.read(buf, 0, HTTPSession.BUFSIZE);
} catch (Exception e) {
safeClose(this.inputStream);
safeClose(this.outputStream);
throw new SocketException("NanoHttpd Shutdown");
}
if (read == -1) {
// socket was been closed
safeClose(this.inputStream);
safeClose(this.outputStream);
throw new SocketException("NanoHttpd Shutdown");
}
while (read > 0) {
this.rlen += read;
this.splitbyte = findHeaderEnd(buf, this.rlen);
if (this.splitbyte > 0)
break;
if (rlen >= buf.length) {
if (NanoHTTPD_debug)
printWithIndent("HEADER> ", new String(buf));
throw fail("Header too big (" + rlen + " bytes)");
}
read = this.inputStream.read(buf, this.rlen, HTTPSession.BUFSIZE - this.rlen);
}
if (this.splitbyte < this.rlen) {
this.inputStream.unread(buf, this.splitbyte, this.rlen - this.splitbyte);
}
this.parms = new HashMap();
if (null == this.headers) {
this.headers = new HashMap();
} else {
this.headers.clear();
}
if (null != this.remoteIp) {
this.headers.put("remote-addr", this.remoteIp);
this.headers.put("http-client-ip", this.remoteIp);
}
// Create a BufferedReader for parsing the header.
BufferedReader hin = new BufferedReader(new InputStreamReader(new ByteArrayInputStream(buf, 0, this.rlen)));
// Decode the header into parms and header java properties
Map pre = new LinkedHashMap();
decodeHeader(hin, pre, this.parms, this.headers, this.files);
this.method = Method.lookup(pre.get("method"));
if (this.method == null) {
throw new ResponseException(Status.BAD_REQUEST, "BAD REQUEST: Syntax error.");
}
this.uri = pre.get("uri");
this.cookies = new CookieHandler(this.headers);
String connection = this.headers.get("connection");
boolean keepAlive = protocolVersion.equals("HTTP/1.1") && (connection == null || !connection.matches("(?i).*close.*"));
// Ok, now do the serve()
r = serve(this);
if (badClient || specialHandler) return;
if (r == null) {
throw new ResponseException(Status.INTERNAL_ERROR, "SERVER INTERNAL ERROR: Serve() returned a null response.");
} else {
String acceptEncoding = this.headers.get("accept-encoding");
this.cookies.unloadQueue(r);
r.setRequestMethod(this.method);
r.setGzipEncoding(useGzipWhenAccepted(r) && acceptEncoding != null && acceptEncoding.contains("gzip"));
r.setKeepAlive(keepAlive);
r.send(this.outputStream);
}
if (!keepAlive || "close".equalsIgnoreCase(r.getHeader("connection"))) {
throw new SocketException("NanoHttpd Shutdown");
}
} catch (SocketException e) {
// throw it out to close socket object (finalAccept)
throw e;
} catch (SocketTimeoutException ste) {
// treat socket timeouts the same way we treat socket exceptions
// i.e. close the stream & finalAccept object by throwing the
// exception up the call stack.
throw ste;
} catch (IOException ioe) {
Response resp = newFixedLengthResponse(Status.INTERNAL_ERROR, NanoHTTPD.MIME_PLAINTEXT, "SERVER INTERNAL ERROR: IOException: " + ioe.getMessage());
resp.send(this.outputStream);
safeClose(this.outputStream);
} catch (ResponseException re) {
Response resp = newFixedLengthResponse(re.getStatus(), NanoHTTPD.MIME_PLAINTEXT, re.getMessage());
resp.send(this.outputStream);
safeClose(this.outputStream);
} finally {
if (badClient)
badClients.add(this);
else if (!specialHandler)
safeClose(r);
this.tempFileManager.clear();
}
}
/**
* 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;
}
/**
* Find the byte positions where multipart boundaries start. This reads
* a large block at a time and uses a temporary buffer to optimize
* (memory mapped) file access.
*/
private int[] getBoundaryPositions(java.nio.ByteBuffer b, byte[] boundary) {
int[] res = new int[0];
if (b.remaining() < boundary.length) {
return res;
}
int search_window_pos = 0;
byte[] search_window = new byte[4 * 1024 + boundary.length];
int first_fill = (b.remaining() < search_window.length) ? b.remaining() : search_window.length;
b.get(search_window, 0, first_fill);
int new_bytes = first_fill - boundary.length;
do {
// Search the search_window
for (int j = 0; j < new_bytes; j++) {
for (int i = 0; i < boundary.length; i++) {
if (search_window[j + i] != boundary[i])
break;
if (i == boundary.length - 1) {
// Match found, add it to results
int[] new_res = new int[res.length + 1];
System.arraycopy(res, 0, new_res, 0, res.length);
new_res[res.length] = search_window_pos + j;
res = new_res;
}
}
}
search_window_pos += new_bytes;
// Copy the end of the buffer to the start
System.arraycopy(search_window, search_window.length - boundary.length, search_window, 0, boundary.length);
// Refill search_window
new_bytes = search_window.length - boundary.length;
new_bytes = (b.remaining() < new_bytes) ? b.remaining() : new_bytes;
b.get(search_window, boundary.length, new_bytes);
} while (new_bytes > 0);
return res;
}
@Override
public CookieHandler getCookies() {
return this.cookies;
}
@Override
public final Map getHeaders() {
return this.headers;
}
@Override
public final Map getFiles() {
return this.files;
}
public Socket getSocket() { return socket; }
@Override
public final InputStream getInputStream() {
return this.inputStream;
}
@Override
public final OutputStream getOutputStream() {
return this.outputStream;
}
@Override
public final Method getMethod() {
return this.method;
}
@Override
public final Map getParms() {
return this.parms;
}
@Override
public String getQueryParameterString() {
return this.queryParameterString;
}
private RandomAccessFile getTmpBucket() {
try {
TempFile tempFile = this.tempFileManager.createTempFile();
return new RandomAccessFile(tempFile.getName(), "rw");
} catch (Exception e) {
throw new Error(e); // we won't recover, so throw an error
}
}
@Override
public final String getUri() {
return this.uri;
}
@Override
public void parseBody() throws IOException, ResponseException {
final int REQUEST_BUFFER_LEN = 512;
final int MEMORY_STORE_LIMIT = 1024;
RandomAccessFile randomAccessFile = null;
try {
long size;
if (this.headers.containsKey("content-length")) {
size = Integer.parseInt(this.headers.get("content-length"));
} else if (this.splitbyte < this.rlen) {
size = this.rlen - this.splitbyte;
} else {
size = 0;
}
ByteArrayOutputStream baos = null;
DataOutput request_data_output = null;
// Store the request in memory or a file, depending on size
if (size < MEMORY_STORE_LIMIT) {
baos = new ByteArrayOutputStream();
request_data_output = new DataOutputStream(baos);
} else {
randomAccessFile = getTmpBucket();
request_data_output = randomAccessFile;
}
// Read all the body and write it to request_data_output
byte[] buf = new byte[REQUEST_BUFFER_LEN];
while (this.rlen >= 0 && size > 0) {
this.rlen = this.inputStream.read(buf, 0, (int) Math.min(size, REQUEST_BUFFER_LEN));
size -= this.rlen;
if (this.rlen > 0) {
request_data_output.write(buf, 0, this.rlen);
}
}
java.nio.ByteBuffer fbuf = null;
if (baos != null) {
fbuf = java.nio.ByteBuffer.wrap(baos.toByteArray(), 0, baos.size());
} else {
fbuf = randomAccessFile.getChannel().map(FileChannel.MapMode.READ_ONLY, 0, randomAccessFile.length());
randomAccessFile.seek(0);
}
// If the method is POST, there may be parameters
// in data section, too, read it:
if (Method.POST.equals(this.method)) {
String contentType = "";
String contentTypeHeader = this.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)) {
// Handle multipart/form-data
if (!st.hasMoreTokens()) {
throw new ResponseException(Status.BAD_REQUEST,
"BAD REQUEST: Content type is multipart/form-data but boundary missing. Usage: GET /example/file.html");
}
String boundaryStartString = "boundary=";
int boundaryContentStart = contentTypeHeader.indexOf(boundaryStartString) + boundaryStartString.length();
String boundary = contentTypeHeader.substring(boundaryContentStart, contentTypeHeader.length());
if (boundary.startsWith("\"") && boundary.endsWith("\"")) {
boundary = boundary.substring(1, boundary.length() - 1);
}
decodeMultipartFormData(boundary, fbuf, this.parms, files);
} else {
byte[] postBytes = new byte[fbuf.remaining()];
if (NanoHTTPD_debug)
print("NanoHTTPD: Handling POST data (" + l(postBytes) + " bytes)");
fbuf.get(postBytes);
String postLine = new String(postBytes).trim();
// Handle application/x-www-form-urlencoded
if ("application/x-www-form-urlencoded".equalsIgnoreCase(contentType)) {
decodeParms(postLine, this.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);
}
}
} else if (Method.PUT.equals(this.method)) {
files.put("content", saveTmpFile(fbuf, 0, fbuf.limit()));
}
} finally {
safeClose(randomAccessFile);
}
}
/**
* Retrieves the content of a sent file and saves it to a temporary
* file. The full path to the saved file is returned.
*/
private String saveTmpFile(java.nio.ByteBuffer b, int offset, int len) {
String path = "";
if (len > 0) {
FileOutputStream fileOutputStream = null;
try {
TempFile tempFile = this.tempFileManager.createTempFile();
java.nio.ByteBuffer src = b.duplicate();
fileOutputStream = new FileOutputStream(tempFile.getName());
FileChannel dest = fileOutputStream.getChannel();
src.position(offset).limit(offset + len);
dest.write(src.slice());
path = tempFile.getName();
} catch (Exception e) { // Catch exception if any
throw new Error(e); // we won't recover, so throw an error
} finally {
safeClose(fileOutputStream);
}
}
return path;
}
public String remoteIp() { return remoteIp; }
} // end of HTTPSession
/**
* Handles one session, i.e. parses the HTTP request and returns the
* response.
*/
public interface IHTTPSession {
void badClient(boolean b);
void specialHandler(boolean b);
void execute() throws IOException;
CookieHandler getCookies();
Map getHeaders();
Map getFiles();
Socket getSocket();
InputStream getInputStream();
OutputStream getOutputStream();
Method getMethod();
String getProtocolVersion();
String getFullURI();
List getHeaderLines();
Map getParms();
String getQueryParameterString();
/**
* @return the path part of the URL.
*/
String getUri();
/**
* Adds the files in the request body to the files map.
*
* @param files
* map to modify
*/
void parseBody() throws IOException, ResponseException;
String remoteIp();
}
/**
* HTTP Request methods, with the ability to decode a String
* back to its enum value.
*/
public enum Method {
GET,
PUT,
POST,
DELETE,
HEAD,
OPTIONS,
TRACE,
CONNECT,
PATCH;
static Method lookup(String method) {
for (Method m : Method.values()) {
if (m.toString().equalsIgnoreCase(method)) {
return m;
}
}
return null;
}
}
/**
* HTTP response. Return one of these from serve().
*/
public static class Response implements Closeable {
public String toString() {
return "HttpResponse " + status + " " + mimeType;
}
/**
* Output stream that will automatically send every write to the wrapped
* OutputStream according to chunked transfer:
* http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.6.1
*/
private static class ChunkedOutputStream extends FilterOutputStream {
public ChunkedOutputStream(OutputStream out) {
super(out);
}
@Override
public void write(int b) throws IOException {
byte[] data = {
(byte) b
};
write(data, 0, 1);
}
@Override
public void write(byte[] b) throws IOException {
write(b, 0, b.length);
}
@Override
public void write(byte[] b, int off, int len) throws IOException {
if (len == 0)
return;
out.write(String.format("%x\r\n", len).getBytes());
out.write(b, off, len);
out.write("\r\n".getBytes());
}
public void finish() throws IOException {
out.write("0\r\n\r\n".getBytes());
}
}
/**
* 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;
private long contentLength;
/**
* Headers for the HTTP response. Use addHeader() to add lines.
*/
//private final Map header = new HashMap();
final Map header = lithashmap("X-Powered-By", "JavaX");
/**
* The request method that spawned this response.
*/
private Method requestMethod;
/**
* Use chunkedTransfer
*/
private boolean chunkedTransfer = false;
private boolean encodeAsGzip = false;
private boolean keepAlive = false;
/**
* Creates a fixed length response if totalBytes>=0, otherwise chunked.
*/
protected Response(IStatus status, String mimeType, InputStream data, long totalBytes) {
this.status = status;
this.mimeType = mimeType;
if (data == null) {
this.data = new ByteArrayInputStream(new byte[0]);
this.contentLength = 0L;
} else {
this.data = data;
this.contentLength = totalBytes;
}
this.chunkedTransfer = this.contentLength < 0;
keepAlive = true;
}
@Override
public void close() throws IOException {
if (this.data != null) {
this.data.close();
}
}
/**
* Adds given line to the header.
*/
public void addHeader(String name, String value) {
this.header.put(name, value);
}
public InputStream getData() {
return this.data;
}
public String getHeader(String name) {
for (String headerName : header.keySet()) {
if (headerName.equalsIgnoreCase(name)) {
return header.get(headerName);
}
}
return null;
}
public String getMimeType() {
return this.mimeType;
}
public Method getRequestMethod() {
return this.requestMethod;
}
public IStatus getStatus() {
return this.status;
}
public void setGzipEncoding(boolean encodeAsGzip) {
this.encodeAsGzip = encodeAsGzip;
}
public void setKeepAlive(boolean useKeepAlive) {
this.keepAlive = useKeepAlive;
}
private boolean headerAlreadySent(Map header, String name) {
boolean alreadySent = false;
for (String headerName : header.keySet()) {
alreadySent |= headerName.equalsIgnoreCase(name);
}
return alreadySent;
}
/**
* Sends given response to the socket.
*/
protected void send(OutputStream outputStream) {
String mime = this.mimeType;
SimpleDateFormat gmtFrmt = new SimpleDateFormat("E, d MMM yyyy HH:mm:ss 'GMT'", Locale.US);
gmtFrmt.setTimeZone(TimeZone.getTimeZone("GMT"));
try {
if (this.status == null) {
throw new Error("sendResponse(): Status can't be null.");
}
PrintWriter pw = new PrintWriter(new BufferedWriter(new OutputStreamWriter(outputStream, "UTF-8")), false);
pw.print("HTTP/1.1 " + this.status.getDescription() + " \r\n");
if (mime != null) {
pw.print("Content-Type: " + mime + "\r\n");
}
if (this.header == null || this.header.get("Date") == null) {
pw.print("Date: " + gmtFrmt.format(new Date()) + "\r\n");
}
if (this.header != null) {
for (String key : this.header.keySet()) {
String value = this.header.get(key);
pw.print(key + ": " + value + "\r\n");
}
}
if (!headerAlreadySent(header, "connection")) {
pw.print("Connection: " + (this.keepAlive ? "keep-alive" : "close") + "\r\n");
}
if (headerAlreadySent(this.header, "content-length")) {
encodeAsGzip = false;
}
if (encodeAsGzip) {
pw.print("Content-Encoding: gzip\r\n");
setChunkedTransfer(true);
}
long pending = this.data != null ? this.contentLength : 0;
if (this.requestMethod != Method.HEAD && this.chunkedTransfer) {
pw.print("Transfer-Encoding: chunked\r\n");
} else if (!encodeAsGzip) {
pending = sendContentLengthHeaderIfNotAlreadyPresent(pw, this.header, pending);
}
pw.print("\r\n");
pw.flush();
sendBodyWithCorrectTransferAndEncoding(outputStream, pending);
outputStream.flush();
safeClose(this.data);
} catch (IOException ioe) {
NanoHTTPD.LOG.log(Level.SEVERE, "Could not send response to the client", ioe);
}
}
private void sendBodyWithCorrectTransferAndEncoding(OutputStream outputStream, long pending) throws IOException {
if (this.requestMethod != Method.HEAD && this.chunkedTransfer) {
ChunkedOutputStream chunkedOutputStream = new ChunkedOutputStream(outputStream);
sendBodyWithCorrectEncoding(chunkedOutputStream, -1);
chunkedOutputStream.finish();
} else {
sendBodyWithCorrectEncoding(outputStream, pending);
}
}
private void sendBodyWithCorrectEncoding(OutputStream outputStream, long pending) throws IOException {
if (encodeAsGzip) {
GZIPOutputStream gzipOutputStream = new GZIPOutputStream(outputStream);
sendBody(gzipOutputStream, -1);
gzipOutputStream.finish();
} else {
sendBody(outputStream, pending);
}
}
/**
* Sends the body to the specified OutputStream. The pending parameter
* limits the maximum amounts of bytes sent unless it is -1, in which
* case everything is sent.
*
* @param outputStream
* the OutputStream to send data to
* @param pending
* -1 to send everything, otherwise sets a max limit to the
* number of bytes sent
* @throws IOException
* if something goes wrong while sending the data.
*/
private void sendBody(OutputStream outputStream, long pending) throws IOException {
long BUFFER_SIZE = 16 * 1024;
byte[] buff = new byte[(int) BUFFER_SIZE];
boolean sendEverything = pending == -1;
while (pending > 0 || sendEverything) {
long bytesToRead = sendEverything ? BUFFER_SIZE : Math.min(pending, BUFFER_SIZE);
int read = this.data.read(buff, 0, (int) bytesToRead);
if (read <= 0) {
break;
}
outputStream.write(buff, 0, read);
if (!sendEverything) {
pending -= read;
}
}
}
protected long sendContentLengthHeaderIfNotAlreadyPresent(PrintWriter pw, Map header, long size) {
for (String headerName : header.keySet()) {
if (headerName.equalsIgnoreCase("content-length")) {
try {
return Long.parseLong(header.get(headerName));
} catch (NumberFormatException ex) {
return size;
}
}
}
pw.print("Content-Length: " + size + "\r\n");
return size;
}
public void setChunkedTransfer(boolean chunkedTransfer) {
this.chunkedTransfer = chunkedTransfer;
}
public void setData(InputStream data) {
this.data = data;
}
public void setMimeType(String mimeType) {
this.mimeType = mimeType;
}
public void setRequestMethod(Method requestMethod) {
this.requestMethod = requestMethod;
}
public void setStatus(IStatus status) {
this.status = status;
}
}
public static final class ResponseException extends Exception {
private static final long serialVersionUID = 6569838532917408380L;
private final Status status;
public ResponseException(Status status, String message) {
super(message);
this.status = status;
}
public ResponseException(Status status, String message, Exception e) {
super(message, e);
this.status = status;
}
public Status getStatus() {
return this.status;
}
}
/**
* The runnable that will be used for the main listening thread.
*/
public class ServerRunnable implements Runnable {
private final int timeout;
private IOException bindException;
private boolean hasBinded = false;
private ServerRunnable(int timeout) {
this.timeout = timeout;
}
@Override
public void run() {
try {
myServerSocket.bind(hostname != null ? new InetSocketAddress(hostname, myPort) : new InetSocketAddress(myPort));
hasBinded = true;
} catch (IOException e) {
print("Was trying to bind to port: " + myPort + (hostname == null ? "" : " on " + hostname));
this.bindException = e;
return;
}
do {
try {
final Socket finalAccept = NanoHTTPD.this.myServerSocket.accept();
if (NanoHTTPD_debug) print("NanoHTTPD: New socket.");
if (this.timeout > 0) {
finalAccept.setSoTimeout(this.timeout);
}
final InputStream inputStream = wrapStuff("SocketInputStream", finalAccept.getInputStream(), finalAccept, NanoHTTPD.this.myServerSocket);
if (NanoHTTPD_debug) print("NanoHTTPD: Have input stream.");
NanoHTTPD.this.asyncRunner.exec(createClientHandler(finalAccept, inputStream));
} catch (IOException e) {
NanoHTTPD.LOG.log(Level.FINE, "Communication with the client broken", e);
}
} while (!NanoHTTPD.this.myServerSocket.isClosed());
}
}
/**
* A temp file.
*
*
* Temp files are responsible for managing the actual temporary storage and
* cleaning themselves up when no longer needed.
*
*/
public interface TempFile {
void delete() throws Exception;
String getName();
OutputStream open() throws Exception;
}
/**
* Temp file manager.
*
*
* Temp file managers are created 1-to-1 with incoming requests, to create
* and cleanup temporary files created as a result of handling the request.
*
*/
public interface TempFileManager {
void clear();
TempFile createTempFile() throws Exception;
}
/**
* Factory to create temp file managers.
*/
public interface TempFileManagerFactory {
TempFileManager create();
}
/**
* 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 int SOCKET_READ_TIMEOUT =
// 5000;
24*3600*1000; // for WebSockets!
/**
* Common MIME type for dynamic content: plain text
*/
public static final String MIME_PLAINTEXT = "text/plain; charset=utf-8";
/**
* Common MIME type for dynamic content: html
*/
public static final String MIME_HTML = "text/html; charset=utf-8";
/**
* 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";
/**
* logger to log to.
*/
private static final Logger LOG = Logger.getLogger(NanoHTTPD.class.getName());
/**
* Creates an SSLSocketFactory for HTTPS. Pass a loaded KeyStore and an
* array of loaded KeyManagers. These objects must properly
* loaded/initialized by the caller.
*/
public static SSLServerSocketFactory makeSSLSocketFactory(KeyStore loadedKeyStore, KeyManager[] keyManagers) throws IOException {
SSLServerSocketFactory res = null;
try {
TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
trustManagerFactory.init(loadedKeyStore);
SSLContext ctx = SSLContext.getInstance("TLS");
ctx.init(keyManagers, trustManagerFactory.getTrustManagers(), null);
res = ctx.getServerSocketFactory();
} catch (Exception e) {
throw new IOException(e.getMessage());
}
return res;
}
/**
* Creates an SSLSocketFactory for HTTPS. Pass a loaded KeyStore and a
* loaded KeyManagerFactory. These objects must properly loaded/initialized
* by the caller.
*/
public static SSLServerSocketFactory makeSSLSocketFactory(KeyStore loadedKeyStore, KeyManagerFactory loadedKeyFactory) throws IOException {
SSLServerSocketFactory res = null;
try {
TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
trustManagerFactory.init(loadedKeyStore);
SSLContext ctx = SSLContext.getInstance("TLS");
ctx.init(loadedKeyFactory.getKeyManagers(), trustManagerFactory.getTrustManagers(), null);
res = ctx.getServerSocketFactory();
} catch (Exception e) {
throw new IOException(e.getMessage());
}
return res;
}
/**
* Creates an SSLSocketFactory for HTTPS. Pass a KeyStore resource with your
* certificate and passphrase
*/
public static SSLServerSocketFactory makeSSLSocketFactory(String keyAndTrustStoreClasspathPath, char[] passphrase) throws IOException {
SSLServerSocketFactory res = null;
try {
KeyStore keystore = KeyStore.getInstance(KeyStore.getDefaultType());
InputStream keystoreStream = NanoHTTPD.class.getResourceAsStream(keyAndTrustStoreClasspathPath);
keystore.load(keystoreStream, passphrase);
TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
trustManagerFactory.init(keystore);
KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
keyManagerFactory.init(keystore, passphrase);
SSLContext ctx = SSLContext.getInstance("TLS");
ctx.init(keyManagerFactory.getKeyManagers(), trustManagerFactory.getTrustManagers(), null);
res = ctx.getServerSocketFactory();
} catch (Exception e) {
throw new IOException(e.getMessage());
}
return res;
}
private static final void safeClose(Object closeable) {
try {
if (closeable != null) {
if (closeable instanceof Closeable) {
((Closeable) closeable).close();
} else if (closeable instanceof Socket) {
((Socket) closeable).close();
} else if (closeable instanceof ServerSocket) {
((ServerSocket) closeable).close();
} else {
throw new IllegalArgumentException("Unknown object to close");
}
}
} catch (IOException e) {
NanoHTTPD.LOG.log(Level.SEVERE, "Could not close", e);
}
}
private final String hostname;
private final int myPort;
private ServerSocket myServerSocket;
private SSLServerSocketFactory sslServerSocketFactory;
private Thread myThread;
/**
* Pluggable strategy for asynchronously executing requests.
*/
protected AsyncRunner asyncRunner;
/**
* Pluggable strategy for creating and cleaning up temporary files.
*/
private TempFileManagerFactory tempFileManagerFactory;
/**
* Constructs an HTTP server on given port.
*/
public NanoHTTPD(int port) {
this(null, port);
}
// -------------------------------------------------------------------------------
// //
//
// Threading Strategy.
//
// -------------------------------------------------------------------------------
// //
/**
* Constructs an HTTP server on given hostname and port.
*/
public NanoHTTPD(String hostname, int port) {
this.hostname = hostname;
this.myPort = port;
setTempFileManagerFactory(new DefaultTempFileManagerFactory());
setAsyncRunner(new DefaultAsyncRunner());
}
/**
* Forcibly closes all connections that are open.
*/
public synchronized void closeAllConnections() {
stop();
}
/**
* create a instance of the client handler, subclasses can return a subclass
* of the ClientHandler.
*
* @param finalAccept
* the socket the cleint is connected to
* @param inputStream
* the input stream
* @return the client handler
*/
protected ClientHandler createClientHandler(final Socket finalAccept, final InputStream inputStream) {
return new ClientHandler(inputStream, finalAccept);
}
/**
* Instantiate the server runnable, can be overwritten by subclasses to
* provide a subclass of the ServerRunnable.
*
* @param timeout
* the socet timeout to use.
* @return the server runnable.
*/
protected ServerRunnable createServerRunnable(final int timeout) {
return new ServerRunnable(timeout);
}
/**
* 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(NanoHTTPD.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;
// XXX Stefan - allow raw parameters - took the if out:
/* if (propertyValue != null) */ {
parms.get(propertyName).add(propertyValue);
}
}
}
return parms;
}
/**
* 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) {
NanoHTTPD.LOG.log(Level.WARNING, "Encoding not supported, ignored", ignored);
}
return decoded;
}
/**
* @return true if the gzip compression should be used if the client
* accespts it. Default this option is on for text content and off
* for everything else.
*/
protected boolean useGzipWhenAccepted(Response r) {
return r.getMimeType() != null && r.getMimeType().toLowerCase().contains("text/");
}
public final int getListeningPort() {
return this.myServerSocket == null ? -1 : this.myServerSocket.getLocalPort();
}
public final boolean isAlive() {
return wasStarted() && !this.myServerSocket.isClosed() && this.myThread.isAlive();
}
public void join() throws InterruptedException {
myThread.join();
}
/**
* Call before start() to serve over HTTPS instead of HTTP
*/
public void makeSecure(SSLServerSocketFactory sslServerSocketFactory) {
this.sslServerSocketFactory = sslServerSocketFactory;
}
/**
* Create a response with unknown length (using HTTP 1.1 chunking).
*/
public static Response newChunkedResponse(IStatus status, String mimeType, InputStream data) {
return new Response(status, mimeType, data, -1);
}
/**
* Create a response with known length.
*/
public static Response newFixedLengthResponse(IStatus status, String mimeType, InputStream data, long totalBytes) {
return new Response(status, mimeType, data, totalBytes);
}
/**
* Create a text response with known length.
*/
public static Response newFixedLengthResponse(IStatus status, String mimeType, String txt) {
if (txt == null) {
return newFixedLengthResponse(status, mimeType, new ByteArrayInputStream(new byte[0]), 0);
} else {
byte[] bytes;
try {
bytes = txt.getBytes("UTF-8");
} catch (UnsupportedEncodingException e) {
NanoHTTPD.LOG.log(Level.SEVERE, "encoding problem, responding nothing", e);
bytes = new byte[0];
}
return newFixedLengthResponse(status, mimeType, new ByteArrayInputStream(bytes), bytes.length);
}
}
/**
* Create a text response with known length.
*/
public static Response newFixedLengthResponse(String msg) {
return newFixedLengthResponse(Status.OK, NanoHTTPD.MIME_HTML, msg);
}
/**
* 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) {
if (specialHandling(session)) {
session.specialHandler(true);
return null;
}
return serve_2(session);
}
public Response serve_2(IHTTPSession session) {
currentSession.set(session);
Method method = session.getMethod();
if (Method.PUT.equals(method) || Method.POST.equals(method)) {
try {
session.parseBody();
} catch (IOException ioe) {
return newFixedLengthResponse(Status.INTERNAL_ERROR, NanoHTTPD.MIME_PLAINTEXT, "SERVER INTERNAL ERROR: IOException: " + ioe.getMessage());
} catch (ResponseException re) {
return newFixedLengthResponse(re.getStatus(), NanoHTTPD.MIME_PLAINTEXT, re.getMessage());
}
}
Map parms = session.getParms();
if (!noQueryStringParameter)
parms.put(NanoHTTPD.QUERY_STRING_PARAMETER, session.getQueryParameterString());
return serve(session.getUri(), method, session.getHeaders(), parms, session.getFiles());
}
/**
* 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 newFixedLengthResponse(Status.NOT_FOUND, NanoHTTPD.MIME_PLAINTEXT, "Not Found");
}
/**
* Pluggable strategy for asynchronously executing requests.
*
* @param asyncRunner
* new strategy for handling threads.
*/
public void setAsyncRunner(AsyncRunner asyncRunner) {
this.asyncRunner = asyncRunner;
}
/**
* Pluggable strategy for creating and cleaning up temporary files.
*
* @param tempFileManagerFactory
* new strategy for handling temp files.
*/
public void setTempFileManagerFactory(TempFileManagerFactory tempFileManagerFactory) {
this.tempFileManagerFactory = tempFileManagerFactory;
}
/**
* Start the server.
*
* @throws IOException
* if the socket is in use.
*/
public void start() throws IOException {
start(NanoHTTPD.SOCKET_READ_TIMEOUT);
}
/**
* Start the server.
*
* @param timeout
* timeout to use for socket connections.
* @throws IOException
* if the socket is in use.
*/
public void start(final int timeout) throws IOException {
boolean ssl = this.sslServerSocketFactory != null;
if (ssl) {
SSLServerSocket ss = (SSLServerSocket) this.sslServerSocketFactory.createServerSocket();
ss.setNeedClientAuth(false);
this.myServerSocket = ss;
} else {
this.myServerSocket = new ServerSocket();
}
this.myServerSocket.setReuseAddress(true);
ServerRunnable serverRunnable = createServerRunnable(timeout);
this.myThread = new Thread(serverRunnable);
//this.myThread.setDaemon(true);
this.myThread.setName("NanoHttpd Main Listener");
this.myThread.start();
while (!serverRunnable.hasBinded && serverRunnable.bindException == null) {
try {
Thread.sleep(10L);
} catch (Throwable e) {
// on android this may not be allowed, that's why we
// catch throwable the wait should be very short because we are
// just waiting for the bind of the socket
}
}
if (serverRunnable.bindException != null) {
throw serverRunnable.bindException;
}
System.out.println("HTTP" + (ssl ? "S" : "") + " server started (listening on port " + getListeningPort() + "!)");
printMyIPs();
}
/**
* Stop the server.
*/
public void stop() {
try {
safeClose(this.myServerSocket);
this.asyncRunner.closeAll();
if (this.myThread != null) {
this.myThread.join();
}
} catch (Exception e) {
NanoHTTPD.LOG.log(Level.SEVERE, "Could not stop all connections", e);
}
}
public final boolean wasStarted() {
return this.myServerSocket != null && this.myThread != null;
}
static void printMyIPs() {
String ip;
try {
Enumeration interfaces = NetworkInterface.getNetworkInterfaces();
while (interfaces.hasMoreElements()) {
NetworkInterface iface = interfaces.nextElement();
// filters out 127.0.0.1 and inactive interfaces
if (iface.isLoopback() || !iface.isUp())
continue;
Enumeration addresses = iface.getInetAddresses();
while(addresses.hasMoreElements()) {
InetAddress addr = addresses.nextElement();
ip = addr.getHostAddress();
if (ip.startsWith("127.")) continue;
boolean local = addr.isSiteLocalAddress() || ip.startsWith("fe");
System.out.println(iface.getDisplayName() + " " + ip + " " + (local ? "(private address)" : "(public address)"));
}
}
} catch (Throwable e) {
e.printStackTrace();
}
}
static interface IStatus {
String getDescription();
int getRequestStatus();
}
/**
* Some HTTP response status codes
*/
static 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"),
REQUEST_TIMEOUT(408, "Request Timeout"),
RANGE_NOT_SATISFIABLE(416, "Requested Range Not Satisfiable"),
INTERNAL_ERROR(500, "Internal Server Error"),
UNSUPPORTED_HTTP_VERSION(505, "HTTP Version Not Supported");
private final int requestStatus;
private final String description;
Status(int requestStatus, String description) {
this.requestStatus = requestStatus;
this.description = description;
}
@Override
public String getDescription() {
return "" + this.requestStatus + " " + this.description;
}
@Override
public int getRequestStatus() {
return this.requestStatus;
}
}
int getPort() { return myPort; }
public void close() { stop(); }
// plug in proxies etc here
// return true if handled
transient IF1 specialHandling;
boolean specialHandling(IHTTPSession session) { return specialHandling != null ? specialHandling.get(session) : specialHandling_base(session); }
final boolean specialHandling_fallback(IF1 _f, IHTTPSession session) { return _f != null ? _f.get(session) : specialHandling_base(session); }
boolean specialHandling_base(IHTTPSession session) {
return false;
}
} // end of NanoHTTPD
static abstract class F0 {
abstract A get();
}
static abstract class F1 {
abstract B get(A a);
}
// you still need to implement hasNext() and next()
static abstract class IterableIterator implements Iterator, Iterable {
public Iterator iterator() {
return this;
}
public void remove() {
unsupportedOperation();
}
}
// In the newest pinging system (with flag PingV3), a ping source
// is the object that "allows" some code to run.
// When that code calls ping(), the ping source's action (if defined)
// is triggered.
// This allows randomly interrupting code execution, for example.
static class PingSource {
// returns true if it slept
final public PingSource setAction(IF0 action){ return action(action); }
public PingSource action(IF0 action) { this.action = action; return this; } final public IF0 getAction(){ return action(); }
public IF0 action() { return action; }
volatile IF0 action;
// optional description of this ping source
String text;
// optional thread pool that this ping source likes to run in
ThreadPool threadPool;
PingSource() {}
PingSource(ThreadPool threadPool) {
this.threadPool = threadPool;}
PingSource(ThreadPool threadPool, String text) {
this.text = text;
this.threadPool = threadPool;}
PingSource(IF0 action) {
this.action = action;}
// returns true if it slept
final boolean get() {
var a = action;
return a != null && a.get();
}
final void ping() {
var a = action;
if (a != null) a.get();
}
void cancel() {
action = new Cancelled();
}
class Cancelled implements IF0 {
public Boolean get() { throw new PingSourceCancelledException(PingSource.this); }
}
class Encapsulated implements Runnable , IFieldsToList{
Runnable r;
Encapsulated() {}
Encapsulated(Runnable r) {
this.r = r;}public Object[] _fieldsToList() { return new Object[] {r}; }
public void run() { try {
//System.out.println("Encapsulated running: " + r);
try {
pingSource_tl().set(PingSource.this);
//System.out.println("Ping source set");
ping();
r.run();
//System.out.println("Done running");
} finally {
//System.out.println("Finally");
pingSource_tl().set(null);
}
} catch (Exception __e) { throw rethrow(__e); } }
public String toString() { return PingSource.this + ": " + r; }
}
void dO(Runnable r) {
if (r == null) return;
threadPool.acquireThreadOrQueue(new Encapsulated(r));
}
public String toString() { String t = text; return nempty(t) ? t : super.toString(); }
ISleeper_v2 sleeper() { return threadPool.sleeper(); }
}
// elements are put to front when added (not when accessed)
static class MRUCache extends LinkedHashMap {
int maxSize = 10;
MRUCache() {}
MRUCache(int maxSize) {
this.maxSize = maxSize;}
protected boolean removeEldestEntry(Map.Entry eldest) {
return size() > maxSize;
}
Object _serialize() {
return ll(maxSize, cloneLinkedHashMap(this));
}
static MRUCache _deserialize(List l) {
MRUCache m = new MRUCache();
m.maxSize = (int) first(l);
m.putAll((LinkedHashMap) second(l));
return m;
}
}
static class MultiMap implements IMultiMap {
Map> data = new HashMap>();
int fullSize;
MultiMap() {}
MultiMap(boolean useTreeMap) { if (useTreeMap) data = new TreeMap(); }
MultiMap(MultiMap map) { putAll(map); }
MultiMap(Map> data) {
this.data = data;}
void put(A key, B value) { synchronized(data) {
List list = data.get(key);
if (list == null)
data.put(key, list = _makeEmptyList());
list.add(value);
++fullSize;
}}
void add(A key, B value) { put(key, value); }
void addAll(A key, Collection values) { putAll(key, values); }
void addAllIfNotThere(A key, Collection values) { synchronized(data) {
for (B value : values)
setPut(key, value);
}}
void setPut(A key, B value) { synchronized(data) {
if (!containsPair(key, value))
put(key, value);
}}
boolean containsPair(A key, B value) { synchronized(data) {
return get(key).contains(value);
}}
void putAll(Collection keys, B value) { synchronized(data) {
for (A key : unnullForIteration(keys))
put(key, value);
}}
void putAll(A key, Collection values) { synchronized(data) {
if (nempty(values)) getActual(key).addAll(values);
}}
void putAll(Iterable> pairs) { synchronized(data) {
for (Pair p : unnullForIteration(pairs))
put(p.a, p.b);
}}
void removeAll(A key, Collection values) { synchronized(data) {
for (B value : values)
remove(key, value);
}}
public List get(A key) { synchronized(data) {
List list = data.get(key);
return list == null ? Collections. emptyList() : list;
}}
List getOpt(A key) { synchronized(data) {
return data.get(key);
}}
List getAndClear(A key) { synchronized(data) {
List l = cloneList(data.get(key));
remove(key);
return l;
}}
// returns actual mutable live list
// creates the list if not there
List getActual(A key) { synchronized(data) {
List list = data.get(key);
if (list == null)
data.put(key, list = _makeEmptyList());
return list;
}}
void clean(A key) { synchronized(data) {
List list = data.get(key);
if (list != null && list.isEmpty()) {
fullSize -= l(list);
data.remove(key);
}
}}
final public Set keys(){ return keySet(); }
public Set keySet() { synchronized(data) {
return data.keySet();
}}
void remove(A key) { synchronized(data) {
fullSize -= l(this.getOpt(key));
data.remove(key);
}}
final void remove(Pair p){ removePair(p); }
void removePair(Pair p) {
if (p != null) remove(p.a, p.b);
}
void remove(A key, B value) { synchronized(data) {
List list = data.get(key);
if (list != null) {
if (list.remove(value))
fullSize--;
if (list.isEmpty())
data.remove(key);
}
}}
void clear() { synchronized(data) {
data.clear();
}}
boolean containsKey(A key) { synchronized(data) {
return data.containsKey(key);
}}
B getFirst(A key) { synchronized(data) {
List list = get(key);
return list.isEmpty() ? null : list.get(0);
}}
void addAll(MultiMap map) { putAll(map); }
void putAll(MultiMap map) { synchronized(data) {
for (A key : map.keySet())
putAll(key, map.get(key));
}}
void putAll(Map map) { synchronized(data) {
if (map != null) for (Map.Entry e : map.entrySet())
put(e.getKey(), e.getValue());
}}
final public int keyCount(){ return keysSize(); }
public int keysSize() { synchronized(data) { return l(data); }}
// full size - note: expensive operation
final public int fullSize(){ return size(); }
public int size() { synchronized(data) {
return fullSize;
}}
// expensive operation
List reverseGet(B b) { synchronized(data) {
List l = new ArrayList();
for (A key : data.keySet())
if (data.get(key).contains(b))
l.add(key);
return l;
}}
Map> asMap() { synchronized(data) {
return cloneMap(data);
}}
boolean isEmpty() { synchronized(data) { return data.isEmpty(); }}
// override in subclasses
List _makeEmptyList() {
return new ArrayList();
}
// returns live lists
Collection> allLists() {
synchronized(data) {
return new ArrayList(data.values());
}
}
Collection> values() { return allLists(); }
List allValues() {
return concatLists(data.values());
}
Object mutex() { return data; }
public String toString() { return "mm" + str(data); }
}
public static interface IF0 {
A get();
}
static interface Hasher {
int hashCode(A a);
boolean equals(A a, A b);
}
static interface IF1 {
B get(A a);
}
/*
* @(#)WeakHashMap.java 1.5 98/09/30
*
* Copyright 1998 by Sun Microsystems, Inc.,
* 901 San Antonio Road, Palo Alto, California, 94303, U.S.A.
* All rights reserved.
*
* This software is the confidential and proprietary information
* of Sun Microsystems, Inc. ("Confidential Information"). You
* shall not disclose such Confidential Information and shall use
* it only in accordance with the terms of the license agreement
* you entered into with Sun.
*/
// From https://github.com/mernst/plume-lib/blob/df0bfafc3c16848d88f4ea0ef3c8bf3367ae085e/java/src/plume/WeakHasherMap.java
static final class WeakHasherMap extends AbstractMap implements Map {
private Hasher hasher = null;
/*@Pure*/
private boolean keyEquals(Object k1, Object k2) {
return (hasher==null ? k1.equals(k2)
: hasher.equals(k1, k2));
}
/*@Pure*/
private int keyHashCode(Object k1) {
return (hasher==null ? k1.hashCode()
: hasher.hashCode(k1));
}
// The WeakKey class can't be static because it depends on the hasher.
// That in turn means that its methods can't be static.
// However, I need to be able to call the methods such as create() that
// were static in the original version of this code.
// This finesses that.
private /*@Nullable*/ WeakKey WeakKeyCreate(K k) {
if (k == null) return null;
else return new WeakKey(k);
}
private /*@Nullable*/ WeakKey WeakKeyCreate(K k, ReferenceQueue super K> q) {
if (k == null) return null;
else return new WeakKey(k, q);
}
// Cannot be a static class: uses keyHashCode() and keyEquals()
private final class WeakKey extends WeakReference {
private int hash; /* Hashcode of key, stored here since the key
may be tossed by the GC */
private WeakKey(K k) {
super(k);
hash = keyHashCode(k);
}
private /*@Nullable*/ WeakKey create(K k) {
if (k == null) return null;
else return new WeakKey(k);
}
private WeakKey(K k, ReferenceQueue super K> q) {
super(k, q);
hash = keyHashCode(k);
}
private /*@Nullable*/ WeakKey create(K k, ReferenceQueue super K> q) {
if (k == null) return null;
else return new WeakKey(k, q);
}
/* A WeakKey is equal to another WeakKey iff they both refer to objects
that are, in turn, equal according to their own equals methods */
/*@Pure*/
@Override
public boolean equals(/*@Nullable*/ Object o) {
if (o == null) return false; // never happens
if (this == o) return true;
// This test is illegal because WeakKey is a generic type,
// so use the getClass hack below instead.
// if (!(o instanceof WeakKey)) return false;
if (!(o.getClass().equals(WeakKey.class))) return false;
Object t = this.get();
@SuppressWarnings("unchecked")
Object u = ((WeakKey)o).get();
if ((t == null) || (u == null)) return false;
if (t == u) return true;
return keyEquals(t, u);
}
/*@Pure*/
@Override
public int hashCode() {
return hash;
}
}
/* Hash table mapping WeakKeys to values */
private HashMap hash;
/* Reference queue for cleared WeakKeys */
private ReferenceQueue super K> queue = new ReferenceQueue();
/* Remove all invalidated entries from the map, that is, remove all entries
whose keys have been discarded. This method should be invoked once by
each public mutator in this class. We don't invoke this method in
public accessors because that can lead to surprising
ConcurrentModificationExceptions. */
@SuppressWarnings("unchecked")
private void processQueue() {
WeakKey wk;
while ((wk = (WeakKey)queue.poll()) != null) { // unchecked cast
hash.remove(wk);
}
}
/* -- Constructors -- */
/**
* Constructs a new, empty WeakHashMap with the given
* initial capacity and the given load factor.
*
* @param initialCapacity the initial capacity of the
* WeakHashMap
*
* @param loadFactor the load factor of the WeakHashMap
*
* @throws IllegalArgumentException If the initial capacity is less than
* zero, or if the load factor is
* nonpositive
*/
public WeakHasherMap(int initialCapacity, float loadFactor) {
hash = new HashMap(initialCapacity, loadFactor);
}
/**
* Constructs a new, empty WeakHashMap with the given
* initial capacity and the default load factor, which is
* 0.75.
*
* @param initialCapacity the initial capacity of the
* WeakHashMap
*
* @throws IllegalArgumentException If the initial capacity is less than
* zero
*/
public WeakHasherMap(int initialCapacity) {
hash = new HashMap(initialCapacity);
}
/**
* Constructs a new, empty WeakHashMap with the default
* capacity and the default load factor, which is 0.75.
*/
public WeakHasherMap() {
hash = new HashMap();
}
/**
* Constructs a new, empty WeakHashMap with the default
* capacity and the default load factor, which is 0.75.
* The WeakHashMap uses the specified hasher for hashing
* keys and comparing them for equality.
* @param h the Hasher to use when hashing values for this map
*/
public WeakHasherMap(Hasher h) {
hash = new HashMap();
hasher = h;
}
/* -- Simple queries -- */
/**
* Returns the number of key-value mappings in this map.
* Note: In contrast to most implementations of the
* Map interface, the time required by this operation is
* linear in the size of the map.
*/
/*@Pure*/
@Override
public int size() {
return entrySet().size();
}
/**
* Returns true if this map contains no key-value mappings.
*/
/*@Pure*/
@Override
public boolean isEmpty() {
return entrySet().isEmpty();
}
/**
* Returns true if this map contains a mapping for the
* specified key.
*
* @param key the key whose presence in this map is to be tested
*/
/*@Pure*/
@Override
public boolean containsKey(Object key) {
@SuppressWarnings("unchecked")
K kkey = (K) key;
return hash.containsKey(WeakKeyCreate(kkey));
}
/* -- Lookup and modification operations -- */
/**
* Returns the value to which this map maps the specified key.
* If this map does not contain a value for this key, then return
* null.
*
* @param key the key whose associated value, if any, is to be returned
*/
/*@Pure*/
@Override
public /*@Nullable*/ V get(Object key) { // type of argument is Object, not K
@SuppressWarnings("unchecked")
K kkey = (K) key;
return hash.get(WeakKeyCreate(kkey));
}
/**
* Updates this map so that the given key maps to the given
* value. If the map previously contained a mapping for
* key then that mapping is replaced and the previous value is
* returned.
*
* @param key the key that is to be mapped to the given
* value
* @param value the value to which the given key is to be
* mapped
*
* @return the previous value to which this key was mapped, or
* null if if there was no mapping for the key
*/
@Override
public V put(K key, V value) {
processQueue();
return hash.put(WeakKeyCreate(key, queue), value);
}
/**
* Removes the mapping for the given key from this map, if
* present.
*
* @param key the key whose mapping is to be removed
*
* @return the value to which this key was mapped, or null if
* there was no mapping for the key
*/
@Override
public V remove(Object key) { // type of argument is Object, not K
processQueue();
@SuppressWarnings("unchecked")
K kkey = (K) key;
return hash.remove(WeakKeyCreate(kkey));
}
/**
* Removes all mappings from this map.
*/
@Override
public void clear() {
processQueue();
hash.clear();
}
/* -- Views -- */
/* Internal class for entries */
// This can't be static, again because of dependence on hasher.
@SuppressWarnings("TypeParameterShadowing")
private final class Entry implements Map.Entry {
private Map.Entry ent;
private K key; /* Strong reference to key, so that the GC
will leave it alone as long as this Entry
exists */
Entry(Map.Entry ent, K key) {
this.ent = ent;
this.key = key;
}
/*@Pure*/
@Override
public K getKey() {
return key;
}
/*@Pure*/
@Override
public V getValue() {
return ent.getValue();
}
@Override
public V setValue(V value) {
return ent.setValue(value);
}
/*@Pure*/
private boolean keyvalEquals(K o1, K o2) {
return (o1 == null) ? (o2 == null) : keyEquals(o1, o2);
}
/*@Pure*/
private boolean valEquals(V o1, V o2) {
return (o1 == null) ? (o2 == null) : o1.equals(o2);
}
/*@Pure*/
@SuppressWarnings("NonOverridingEquals")
public boolean equals(Map.Entry e /* Object o*/) {
// if (! (o instanceof Map.Entry)) return false;
// Map.Entry e = (Map.Entry)o;
return (keyvalEquals(key, e.getKey())
&& valEquals(getValue(), e.getValue()));
}
/*@Pure*/
@Override
public int hashCode() {
V v;
return (((key == null) ? 0 : keyHashCode(key))
^ (((v = getValue()) == null) ? 0 : v.hashCode()));
}
}
/* Internal class for entry sets */
private final class EntrySet extends AbstractSet> {
Set> hashEntrySet = hash.entrySet();
@Override
public Iterator> iterator() {
return new Iterator>() {
Iterator> hashIterator = hashEntrySet.iterator();
Map.Entry next = null;
@Override
public boolean hasNext() {
while (hashIterator.hasNext()) {
Map.Entry ent = hashIterator.next();
WeakKey wk = ent.getKey();
K k = null;
if ((wk != null) && ((k = wk.get()) == null)) {
/* Weak key has been cleared by GC */
continue;
}
next = new Entry(ent, k);
return true;
}
return false;
}
@Override
public Map.Entry next() {
if ((next == null) && !hasNext())
throw new NoSuchElementException();
Map.Entry e = next;
next = null;
return e;
}
@Override
public void remove() {
hashIterator.remove();
}
};
}
/*@Pure*/
@Override
public boolean isEmpty() {
return !(iterator().hasNext());
}
/*@Pure*/
@Override
public int size() {
int j = 0;
for (Iterator> i = iterator(); i.hasNext(); i.next()) j++;
return j;
}
@Override
public boolean remove(Object o) {
processQueue();
if (!(o instanceof Map.Entry,?>)) return false;
@SuppressWarnings("unchecked")
Map.Entry e = (Map.Entry)o; // unchecked cast
Object ev = e.getValue();
WeakKey wk = WeakKeyCreate(e.getKey());
Object hv = hash.get(wk);
if ((hv == null)
? ((ev == null) && hash.containsKey(wk)) : hv.equals(ev)) {
hash.remove(wk);
return true;
}
return false;
}
/*@Pure*/
@Override
public int hashCode() {
int h = 0;
for (Iterator> i = hashEntrySet.iterator(); i.hasNext(); ) {
Map.Entry ent = i.next();
WeakKey wk = ent.getKey();
Object v;
if (wk == null) continue;
h += (wk.hashCode()
^ (((v = ent.getValue()) == null) ? 0 : v.hashCode()));
}
return h;
}
}
private /*@Nullable*/ Set> entrySet = null;
/**
* Returns a Set view of the mappings in this map.
*/
/*@SideEffectFree*/
@Override
public Set> entrySet() {
if (entrySet == null) entrySet = new EntrySet();
return entrySet;
}
// find matching key
K findKey(Object key) {
processQueue();
K kkey = (K) key;
// TODO: use replacement for HashMap to avoid reflection
WeakKey wkey = WeakKeyCreate(kkey);
WeakKey found = hashMap_findKey(hash, wkey);
return found == null ? null : found.get();
}
}
static class PersistableThrowable extends DynamicObject {
String className;
String msg;
String stacktrace;
PersistableThrowable() {}
PersistableThrowable(Throwable e) {
if (e == null)
className = "Crazy Null Error";
else {
className = getClassName(e).replace('/', '.');
msg = e.getMessage();
stacktrace = getStackTrace_noRecord(e);
}
}
public String toString() {
return nempty(msg) ? className + ": " + msg : className;
}
RuntimeException asRuntimeException() {
return new Fail(this);
}
}
static interface IVF1 {
void get(A a);
}
static class Pair implements Comparable> {
final public Pair setA(A a){ return a(a); }
public Pair a(A a) { this.a = a; return this; } final public A getA(){ return a(); }
public A a() { return a; }
A a;
final public Pair setB(B b){ return b(b); }
public Pair b(B b) { this.b = b; return this; } final public B getB(){ return b(); }
public B b() { return b; }
B b;
Pair() {}
Pair(A a, B b) {
this.b = b;
this.a = a;}
public int hashCode() {
return hashCodeFor(a) + 2*hashCodeFor(b);
}
public boolean equals(Object o) {
if (o == this) return true;
if (!(o instanceof Pair)) return false;
Pair t = (Pair) o;
return eq(a, t.a) && eq(b, t.b);
}
public String toString() {
return "<" + a + ", " + b + ">";
}
public int compareTo(Pair p) {
if (p == null) return 1;
int i = ((Comparable) a).compareTo(p.a);
if (i != 0) return i;
return ((Comparable) b).compareTo(p.b);
}
}
static class Fail extends RuntimeException implements IFieldsToList{
Object[] objects;
Fail() {}
Fail(Object... objects) {
this.objects = objects;}public Object[] _fieldsToList() { return new Object[] {objects}; }
Fail(Throwable cause, Object... objects) {
super(cause);
this.objects = objects;
}
public String toString() { return joinNemptiesWithColon("Fail", commaCombine(getCause(), objects)); }
}
static class PingSourceCancelledException extends RuntimeException implements IFieldsToList{
PingSource pingSource;
PingSourceCancelledException() {}
PingSourceCancelledException(PingSource pingSource) {
this.pingSource = pingSource;}
public String toString() { return shortClassName_dropNumberPrefix(this) + "(" + pingSource + ")"; }public Object[] _fieldsToList() { return new Object[] {pingSource}; }
}
static interface IFieldsToList {
Object[] _fieldsToList();
}
static interface ISleeper_v2 {
Sleeping doLater(Timestamp targetTime, Runnable r);
public default Sleeping doAfter(double seconds, Runnable r) {
return doLater(tsNow().plusSeconds(seconds), r);
}
}
// The idea is to leave max as the actual number of cores the system
// has (numberOfCores()), and in case of being fully booked, raise an
// alert (customerMustWaitAlert) which can be handled by a strategy
// object (different reactions are possible).
// If nothing is done in such an event, clients are processed serially
// (no guarantees of order), split up among the available threads.
/* SYNChronisation order:
1. PooledThread
2. ThreadPool */
static class ThreadPool implements AutoCloseable {
int max = numberOfCores();
List all = new ArrayList();
Set used = new HashSet();
Set free = new HashSet();
boolean verbose, retired;
// our own ping surce so we can start threads & keep them running
class InternalPingSource extends PingSource {}
InternalPingSource internalPingSource = new InternalPingSource();
MultiSleeper sleeper = new MultiSleeper();
ThreadPool() {}
ThreadPool(int max) {
this.max = max;}
synchronized int maxSize() { return max; }
synchronized int total() { return l(used)+l(free); }
transient Set onCustomerMustWaitAlert;
public ThreadPool onCustomerMustWaitAlert(Runnable r) { onCustomerMustWaitAlert = createOrAddToSyncLinkedHashSet(onCustomerMustWaitAlert, r); return this; }
public ThreadPool removeCustomerMustWaitAlertListener(Runnable r) { main.remove(onCustomerMustWaitAlert, r); return this; }
public void customerMustWaitAlert() { if (onCustomerMustWaitAlert != null) for (var listener : onCustomerMustWaitAlert) pcallF_typed(listener); }
void fireCustomerMustWaitAlert() {
vmBus_send("customerMustWaitAlert", this, currentThread());
customerMustWaitAlert();
}
// DOESN'T WAIT. adds action to a thread's queue if nothing is
// available immediately.
PooledThread acquireThreadOrQueue(Runnable action) {
if (action == null) return null;
PooledThread t;
synchronized(this) {
if (_hasFreeAfterCreating()) {
t = _firstFreeThread();
markUsed(t);
} else
t = _anyThread();
}
t.addWork(action); // will move it from free to used
return t;
}
// run in synchronized block
boolean _hasFreeAfterCreating() {
checkNotRetired();
if (nempty(free)) return true;
if (total() < max) {
PooledThread t = newThread();
all.add(t);
free.add(t);
return true;
}
return false;
}
// WAITS until thread is available
PooledThread acquireThreadOrWait(Runnable action) { try {
if (action == null) return null;
PooledThread t;
while (true) {
synchronized(this) {
if (_hasFreeAfterCreating()) {
t = _firstFreeThread();
break;
} else
_waitWaitWait();
}
}
t.addWork(action);
return t;
} catch (Exception __e) { throw rethrow(__e); } }
PooledThread _firstFreeThread() {
return first(free);
}
PooledThread _anyThread() {
return random(used);
}
class PooledThread extends Thread {
PooledThread(String name) { super(name); }
AppendableChain q;
synchronized Runnable _grabWorkOrSleep() { try {
Runnable r = first(q);
if (r == null) {
markFree(this);
if (verbose) print("Thread sleeps");
synchronized(this) { wait(); }
if (verbose) print("Thread woke up");
return null;
}
q = popFirst(q);
return r;
} catch (Exception __e) { throw rethrow(__e); } }
public void run() { try {
pingSource_tl().set(internalPingSource);
while (!retired()) { ping();
Runnable r = _grabWorkOrSleep();
if (verbose) print(this + " work: " + r);
if (r != null)
try {
if (verbose) print(this + " running: " + r);
r.run();
pingSource_tl().set(internalPingSource);
if (verbose) print(this + " done");
} catch (Throwable e) {
pingSource_tl().set(internalPingSource);
if (verbose) print(this + " error");
printStackTrace(e);
} finally {
pingSource_tl().set(internalPingSource);
if (verbose) print("ThreadPool finally");
}
}
} catch (Exception __e) { throw rethrow(__e); } }
synchronized boolean isEmpty() { return empty(q); }
// append to q (do later)
void addWork(Runnable r) {
if (verbose) print("Added work to " + this + ": " + r);
synchronized(this) {
q = chainPlus(q, r);
notifyAll();
}
}
}
PooledThread newThread() {
PooledThread t = new PooledThread("Thread Pool Inhabitant " + n2(total()+1));
t.start();
return t;
}
synchronized void markFree(PooledThread t) {
used.remove(t);
free.add(t);
notifyAll();
}
synchronized void markUsed(PooledThread t) {
free.remove(t);
used.add(t);
}
synchronized public String toString() {
return retired()
? "Retired ThreadPool"
: "ThreadPool " + roundBracket(commaCombine(
n2(used) + " used out of " + n2(total()),
max <= total() ? null : "could grow to " + n2(max)));
}
synchronized boolean retired() { return retired; }
synchronized void retire() {
if (verbose) print("ThreadPool Retiring");
retired = true;
for (var thread : free) syncNotifyAll(thread); // wake it up so it exits
}
void checkNotRetired() {
if (retired()) throw fail("retired");
}
// We could do a soft-close here (stop the idle threads, let running threads finish, then end those too, stop accepting new orders)
// or a hard close (interrupt all threads, stop accepting new orders)
synchronized public void close() { try {
retire();
} catch (Exception __e) { throw rethrow(__e); } }
// run in synchronized block
void _waitWaitWait() { try {
do {
fireCustomerMustWaitAlert();
wait();
checkNotRetired();
} while (empty(free));
} catch (Exception __e) { throw rethrow(__e); } }
void dO(String text, Runnable r) {
if (r == null) return;
new PingSource(this, text).dO(r);
}
ISleeper_v2 sleeper() { return sleeper; }
}
interface IMultiMap {
public Set keySet();
public Collection get(A a);
public int size();
public int keyCount();
}
interface IDoublePt {
public double x_double();
public double y_double();
}
static interface WidthAndHeight {
default int w(){ return getWidth(); }
int getWidth();
default int h(){ return getHeight(); }
int getHeight();
public default Rect bounds() { return rect(0, 0, getWidth(), getHeight()); }
default int area() { return toInt(areaAsLong()); }
default long areaAsLong() { return longMul(w(), h()); }
}
abstract static class Sleeping implements AutoCloseable , IFieldsToList{
Timestamp targetTime;
Runnable action;
Sleeping() {}
Sleeping(Timestamp targetTime, Runnable action) {
this.action = action;
this.targetTime = targetTime;}
public String toString() { return shortClassName_dropNumberPrefix(this) + "(" + targetTime + ", " + action + ")"; }public Object[] _fieldsToList() { return new Object[] {targetTime, action}; }
long remainingMS() { return targetTime.minus(tsNow()); }
}
// AppendableChain has one "smart" head element (with size counter
// and pointer to the chain's last element), all the other nodes are
// maximally simple (MinimalChain).
// This allows O(1) front insertion, front removal and back insertion
// (not removal at the back though) which is fine for what I need this
// for (event queues).
//
// Stefan Reich, Oct 21
static class AppendableChain extends MinimalChain implements Iterable {
MinimalChain last; // pointer to last element in chain (which may be us)
int size; // total length of chain
AppendableChain() {} // only used internally
AppendableChain(A element) {
this.element = element; size = 1; last = this; }
// intermediate constructor called by itemPlusChain()
AppendableChain(A element, AppendableChain next) {
this.next = next;
this.element = element;
if (next == null) return;
MinimalChain b = new MinimalChain();
b.element = next.element;
b.next = next.next;
this.next = b;
last = next.last;
size = next.size+1;
}
public String toString() { return str(toList()); }
// append at the end
boolean add(A a) {
MinimalChain newLast = new MinimalChain(a);
last.next = newLast;
last = newLast;
++size;
return true;
}
// drop first element
AppendableChain popFirst() {
if (next == null) return null;
element = next.element;
if (last == next) last = this;
next = next.next;
--size;
return this;
}
ArrayList toList() {
ArrayList l = emptyList(size);
MinimalChain c = this;
while (c != null) {
l.add(c.element);
c = c.next;
}
return l;
}
//public Iterator iterator() { ret toList().iterator(); }
class ACIt extends IterableIterator < A > {
MinimalChain c = AppendableChain.this;
public boolean hasNext() {
return c != null;
}
public A next() {
var a = c.element;
c = c.next;
return a;
}
}
public IterableIterator iterator() {
return new ACIt();
}
}
static class MultiSleeper extends RestartableCountdown implements ISleeper_v2 {
TreeMultiMap entries = new TreeMultiMap();
void check() {
var time = nextWakeUpTime();
var action = firstValue(entries);
setTargetTime(time == null ? 0 : time.sysTime(), new Runnable() { public void run() { try {
List toCall;
synchronized(MultiSleeper.this) {
toCall = entries.get(time);
entries.remove(time);
}
check();
pcallFAll(toCall);
} catch (Exception __e) { throw rethrow(__e); } } public String toString() { return "List toCall;\r\n synchronized(MultiSleeper.this) {\r\n toCa..."; }});
}
synchronized void removeEntry(Timestamp targetTime, Runnable action) {
entries.remove(targetTime, action);
}
// API
synchronized Timestamp nextWakeUpTime() {
return firstKey(entries);
}
public synchronized Sleeping doLater(Timestamp targetTime, Runnable r) {
if (r == null || targetTime == null) return null;
targetTime = max(targetTime, tsNow());
entries.put(targetTime, r);
check();
return new Sleeping(targetTime, r) {
public void close() { try {
removeEntry(targetTime, r);
} catch (Exception __e) { throw rethrow(__e); } }
};
}
}
static class Timestamp implements Comparable , IFieldsToList{
long date;
Timestamp(long date) {
this.date = date;}
public boolean equals(Object o) {
if (!(o instanceof Timestamp)) return false;
Timestamp __1 = (Timestamp) o;
return date == __1.date;
}
public int hashCode() {
int h = 2059094262;
h = boostHashCombine(h, _hashCode(date));
return h;
}
public Object[] _fieldsToList() { return new Object[] {date}; }
Timestamp() { date = now(); }
long unixDate() { return date; }
public String toString() { return formatLocalDateWithSeconds(date); }
// Hmm. Should Timestamp(0) be equal to null? Question, questions...
public int compareTo(Timestamp t) {
return t == null ? 1 : cmp(date, t.date);
}
Timestamp plus(Seconds seconds) {
return plus(seconds == null ? null : seconds.getDouble());
}
final Timestamp plusSeconds(double seconds){ return plus(seconds); }
Timestamp plus(double seconds) {
return new Timestamp(date+toMS(seconds));
}
// returns milliseconds
long minus(Timestamp ts) {
return unixDate()-ts.unixDate();
}
long sysTime() {
return clockTimeToSystemTime(date);
}
Duration minusAsDuration(Timestamp ts) {
return Duration.ofMillis(minus(ts));
}
}
static class MinimalChain implements Iterable {
A element;
MinimalChain next;
MinimalChain() {}
MinimalChain(A element) {
this.element = element;}
MinimalChain(A element, MinimalChain next) {
this.next = next;
this.element = element;}
public String toString() { return str(toList()); }
ArrayList toList() {
ArrayList l = new ArrayList();
MinimalChain c = this;
while (c != null) {
l.add(c.element);
c = c.next;
}
return l;
}
void setElement(A a) { element = a; }
void setNext(MinimalChain next) { this.next = next; }
// TODO: optimize
public Iterator iterator() { return toList().iterator(); }
A get() { return element; }
}
static class RestartableCountdown implements AutoCloseable {
java.util.Timer timer;
long targetTime; // in sys time
long /*firings,*/ totalSleepTime; // stats
synchronized void setTargetTime(long targetTime, Runnable action) {
if (targetTime <= 0)
stop();
else if (targetTime != this.targetTime) {
start(targetTime-sysNow(), action);
this.targetTime = targetTime;
}
}
// stops the countdown and restarts it
synchronized void start(long delayMS, Object action) {
stop();
if (delayMS <= 0)
{ startThread(new Runnable() { public void run() { try { callF(action);
} catch (Exception __e) { throw rethrow(__e); } } public String toString() { return "callF(action);"; }}); }
else {
totalSleepTime += delayMS;
timer = doLater_daemon(delayMS, action);
targetTime = sysNow()+delayMS;
}
}
void start(double delaySeconds, Object action) {
start(toMS(delaySeconds), action);
}
synchronized void stop() {
cancelTimer(timer);
timer = null;
targetTime = 0;
}
public void close() { stop(); }
}
static class Seconds implements Comparable , IFieldsToList{
double seconds;
Seconds() {}
Seconds(double seconds) {
this.seconds = seconds;}
public boolean equals(Object o) {
if (!(o instanceof Seconds)) return false;
Seconds __1 = (Seconds) o;
return seconds == __1.seconds;
}
public int hashCode() {
int h = -660217249;
h = boostHashCombine(h, _hashCode(seconds));
return h;
}
public Object[] _fieldsToList() { return new Object[] {seconds}; }
final double get(){ return seconds(); }
final double getDouble(){ return seconds(); }
double seconds() { return seconds; }
public String toString() { return formatDouble(seconds, 3) + " s"; }
public int compareTo(Seconds s) {
return cmp(seconds, s.seconds);
}
Seconds div(double x) { return new Seconds(get()/x); }
Seconds minus(Seconds x) { return new Seconds(get()-x.get()); }
}
static class TreeMultiMap extends MultiMap {
TreeMultiMap() { super(true); }
TreeMultiMap(MultiMap map) { this(); putAll(map); }
}
static Class> getClass(String name) {
return _getClass(name);
}
static Class getClass(Object o) {
return _getClass(o);
}
static Class getClass(Object realm, String name) {
return _getClass(realm, name);
}
static boolean classIsExportedTo(Class c, java.lang.Module destModule) {
if (c == null || destModule == null) return false;
java.lang.Module srcModule = c.getModule();
String packageName = c.getPackageName();
return srcModule.isExported(packageName, destModule);
}
static boolean isAbstract(Class c) {
return (c.getModifiers() & Modifier.ABSTRACT) != 0;
}
static boolean isAbstract(Method m) {
return (m.getModifiers() & Modifier.ABSTRACT) != 0;
}
static boolean reflection_isForbiddenMethod(Method m) {
return m.getDeclaringClass() == Object.class
&& eqOneOf(m.getName(), "finalize", "clone", "registerNatives");
}
static Set allInterfacesImplementedBy(Object o) {
return allInterfacesImplementedBy(_getClass(o));
}
static Set allInterfacesImplementedBy(Class c) {
if (c == null) return null;
HashSet