JLHTTP in JPMS
ci/woodpecker/push/woodpecker Pipeline was successful
Details
ci/woodpecker/push/woodpecker Pipeline was successful
Details
This commit is contained in:
parent
c35369bfde
commit
28b08998b1
|
@ -0,0 +1,17 @@
|
|||
This component of JfCommons is based on JLHTTP, which is GPL-licensed:
|
||||
|
||||
Copyright (C) 2021 Curt Cox
|
||||
|
||||
This program is free software; you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation; either version 2 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License along
|
||||
with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
|
@ -0,0 +1,19 @@
|
|||
import io.gitlab.jfronny.scripts.*
|
||||
|
||||
plugins {
|
||||
id("commons.library")
|
||||
}
|
||||
|
||||
dependencies {
|
||||
}
|
||||
|
||||
publishing {
|
||||
publications {
|
||||
create<MavenPublication>("maven") {
|
||||
groupId = "io.gitlab.jfronny"
|
||||
artifactId = "commons-jlhttp"
|
||||
|
||||
from(components["java"])
|
||||
}
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,33 @@
|
|||
package io.gitlab.jfronny.commons.jlhttp.api;
|
||||
|
||||
import io.gitlab.jfronny.commons.jlhttp.util.VirtualHost;
|
||||
|
||||
import java.lang.annotation.*;
|
||||
|
||||
/**
|
||||
* The {@code Context} annotation decorates methods which are mapped
|
||||
* to a context (path) within the server, and provide its contents.
|
||||
* <p>
|
||||
* The annotated methods must have the same signature and contract
|
||||
* as {@link ContextHandler#serve}, but can have arbitrary names.
|
||||
*
|
||||
* @see VirtualHost#addContexts(Object)
|
||||
*/
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Target(ElementType.METHOD)
|
||||
public @interface Context {
|
||||
|
||||
/**
|
||||
* The context (path) that this field maps to (must begin with '/').
|
||||
*
|
||||
* @return the context (path) that this field maps to
|
||||
*/
|
||||
String value();
|
||||
|
||||
/**
|
||||
* The HTTP methods supported by this context handler (default is "GET").
|
||||
*
|
||||
* @return the HTTP methods supported by this context handler
|
||||
*/
|
||||
String[] methods() default "GET";
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
package io.gitlab.jfronny.commons.jlhttp.api;
|
||||
|
||||
import io.gitlab.jfronny.commons.jlhttp.JLHTTPServer;
|
||||
import io.gitlab.jfronny.commons.jlhttp.util.VirtualHost;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* A {@code ContextHandler} serves the content of resources within a context.
|
||||
*
|
||||
* @see VirtualHost#addContext
|
||||
*/
|
||||
public interface ContextHandler {
|
||||
|
||||
/**
|
||||
* Serves the given request using the given response.
|
||||
*
|
||||
* @param req the request to be served
|
||||
* @param resp the response to be filled
|
||||
* @return an HTTP status code, which will be used in returning
|
||||
* a default response appropriate for this status. If this
|
||||
* method invocation already sent anything in the response
|
||||
* (headers or content), it must return 0, and no further
|
||||
* processing will be done
|
||||
* @throws IOException if an IO error occurs
|
||||
*/
|
||||
int serve(JLHTTPServer.Request req, JLHTTPServer.Response resp) throws IOException;
|
||||
}
|
|
@ -0,0 +1,86 @@
|
|||
package io.gitlab.jfronny.commons.jlhttp.io;
|
||||
|
||||
import io.gitlab.jfronny.commons.jlhttp.*;
|
||||
import io.gitlab.jfronny.commons.jlhttp.util.Headers;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
|
||||
/**
|
||||
* The {@code ChunkedInputStream} decodes an InputStream whose data has the
|
||||
* "chunked" transfer encoding applied to it, providing the underlying data.
|
||||
*/
|
||||
public class ChunkedInputStream extends LimitedInputStream {
|
||||
|
||||
protected Headers headers;
|
||||
protected boolean initialized;
|
||||
|
||||
/**
|
||||
* Constructs a ChunkedInputStream with the given underlying stream, and
|
||||
* a headers container to which the stream's trailing headers will be
|
||||
* added.
|
||||
*
|
||||
* @param in the underlying "chunked"-encoded input stream
|
||||
* @param headers the headers container to which the stream's trailing
|
||||
* headers will be added, or null if they are to be discarded
|
||||
* @throws NullPointerException if the given stream is null
|
||||
*/
|
||||
public ChunkedInputStream(InputStream in, Headers headers) {
|
||||
super(in, 0, true);
|
||||
this.headers = headers;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int read() throws IOException {
|
||||
return limit <= 0 && initChunk() < 0 ? -1 : super.read();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int read(byte[] b, int off, int len) throws IOException {
|
||||
return limit <= 0 && initChunk() < 0 ? -1 : super.read(b, off, len);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the next chunk. If the previous chunk has not yet
|
||||
* ended, or the end of stream has been reached, does nothing.
|
||||
*
|
||||
* @return the length of the chunk, or -1 if the end of stream
|
||||
* has been reached
|
||||
* @throws IOException if an IO error occurs or the stream is corrupt
|
||||
*/
|
||||
protected long initChunk() throws IOException {
|
||||
if (limit == 0) { // finished previous chunk
|
||||
// read chunk-terminating CRLF if it's not the first chunk
|
||||
if (initialized && JLHTTPServer.readLine(in).length() > 0)
|
||||
throw new IOException("chunk data must end with CRLF");
|
||||
initialized = true;
|
||||
limit = parseChunkSize(JLHTTPServer.readLine(in)); // read next chunk size
|
||||
if (limit == 0) { // last chunk has size 0
|
||||
limit = -1; // mark end of stream
|
||||
// read trailing headers, if any
|
||||
Headers trailingHeaders = JLHTTPServer.readHeaders(in);
|
||||
if (headers != null)
|
||||
headers.addAll(trailingHeaders);
|
||||
}
|
||||
}
|
||||
return limit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a chunk-size line.
|
||||
*
|
||||
* @param line the chunk-size line to parse
|
||||
* @return the chunk size
|
||||
* @throws IllegalArgumentException if the chunk-size line is invalid
|
||||
*/
|
||||
protected static long parseChunkSize(String line) throws IllegalArgumentException {
|
||||
int pos = line.indexOf(';');
|
||||
line = pos < 0 ? line : line.substring(0, pos); // ignore params, if any
|
||||
try {
|
||||
return JLHTTPServer.parseULong(line, 16); // throws NFE
|
||||
} catch (NumberFormatException nfe) {
|
||||
throw new IllegalArgumentException(
|
||||
"invalid chunk size line: \"" + line + "\"");
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,112 @@
|
|||
package io.gitlab.jfronny.commons.jlhttp.io;
|
||||
|
||||
import io.gitlab.jfronny.commons.jlhttp.util.Headers;
|
||||
import io.gitlab.jfronny.commons.jlhttp.JLHTTPServer;
|
||||
|
||||
import java.io.*;
|
||||
|
||||
/**
|
||||
* The {@code ChunkedOutputStream} encodes an OutputStream with the
|
||||
* "chunked" transfer encoding. It should be used only when the content
|
||||
* length is not known in advance, and with the response Transfer-Encoding
|
||||
* header set to "chunked".
|
||||
* <p>
|
||||
* Data is written to the stream by calling the {@link #write(byte[], int, int)}
|
||||
* method, which writes a new chunk per invocation. To end the stream,
|
||||
* the {@link #writeTrailingChunk} method must be called or the stream closed.
|
||||
*/
|
||||
public class ChunkedOutputStream extends FilterOutputStream {
|
||||
|
||||
protected int state; // the current stream state
|
||||
|
||||
/**
|
||||
* Constructs a ChunkedOutputStream with the given underlying stream.
|
||||
*
|
||||
* @param out the underlying output stream to which the chunked stream
|
||||
* is written
|
||||
* @throws NullPointerException if the given stream is null
|
||||
*/
|
||||
public ChunkedOutputStream(OutputStream out) {
|
||||
super(out);
|
||||
if (out == null)
|
||||
throw new NullPointerException("output stream is null");
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes a new chunk with the given size.
|
||||
*
|
||||
* @param size the chunk size (must be positive)
|
||||
* @throws IllegalArgumentException if size is negative
|
||||
* @throws IOException if an IO error occurs, or the stream has
|
||||
* already been ended
|
||||
*/
|
||||
protected void initChunk(long size) throws IOException {
|
||||
if (size < 0)
|
||||
throw new IllegalArgumentException("invalid size: " + size);
|
||||
if (state > 0)
|
||||
out.write(JLHTTPServer.CRLF); // end previous chunk
|
||||
else if (state == 0)
|
||||
state = 1; // start first chunk
|
||||
else
|
||||
throw new IOException("chunked stream has already ended");
|
||||
out.write(JLHTTPServer.getBytes(Long.toHexString(size)));
|
||||
out.write(JLHTTPServer.CRLF);
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes the trailing chunk which marks the end of the stream.
|
||||
*
|
||||
* @param headers the (optional) trailing headers to write, or null
|
||||
* @throws IOException if an error occurs
|
||||
*/
|
||||
public void writeTrailingChunk(Headers headers) throws IOException {
|
||||
initChunk(0); // zero-sized chunk marks the end of the stream
|
||||
if (headers == null)
|
||||
out.write(JLHTTPServer.CRLF); // empty header block
|
||||
else
|
||||
headers.writeTo(out);
|
||||
state = -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes a chunk containing the given byte. This method initializes
|
||||
* a new chunk of size 1, and then writes the byte as the chunk data.
|
||||
*
|
||||
* @param b the byte to write as a chunk
|
||||
* @throws IOException if an error occurs
|
||||
*/
|
||||
@Override
|
||||
public void write(int b) throws IOException {
|
||||
write(new byte[]{(byte) b}, 0, 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes a chunk containing the given bytes. This method initializes
|
||||
* a new chunk of the given size, and then writes the chunk data.
|
||||
*
|
||||
* @param b an array containing the bytes to write
|
||||
* @param off the offset within the array where the data starts
|
||||
* @param len the length of the data in bytes
|
||||
* @throws IOException if an error occurs
|
||||
* @throws IndexOutOfBoundsException if the given offset or length
|
||||
* are outside the bounds of the given array
|
||||
*/
|
||||
@Override
|
||||
public void write(byte[] b, int off, int len) throws IOException {
|
||||
if (len > 0) // zero-sized chunk is the trailing chunk
|
||||
initChunk(len);
|
||||
out.write(b, off, len);
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes the trailing chunk if necessary, and closes the underlying stream.
|
||||
*
|
||||
* @throws IOException if an error occurs
|
||||
*/
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
if (state > -1)
|
||||
writeTrailingChunk(null);
|
||||
super.close();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,79 @@
|
|||
package io.gitlab.jfronny.commons.jlhttp.io;
|
||||
|
||||
import java.io.*;
|
||||
|
||||
/**
|
||||
* The {@code LimitedInputStream} provides access to a limited number
|
||||
* of consecutive bytes from the underlying InputStream, starting at its
|
||||
* current position. If this limit is reached, it behaves as though the end
|
||||
* of stream has been reached (although the underlying stream remains open
|
||||
* and may contain additional data).
|
||||
*/
|
||||
public class LimitedInputStream extends FilterInputStream {
|
||||
|
||||
protected long limit; // decremented when read, until it reaches zero
|
||||
protected boolean prematureEndException;
|
||||
|
||||
/**
|
||||
* Constructs a LimitedInputStream with the given underlying
|
||||
* input stream and limit.
|
||||
*
|
||||
* @param in the underlying input stream
|
||||
* @param limit the maximum number of bytes that may be consumed from
|
||||
* the underlying stream before this stream ends. If zero or
|
||||
* negative, this stream will be at its end from initialization.
|
||||
* @param prematureEndException specifies the stream's behavior when
|
||||
* the underlying stream end is reached before the limit is
|
||||
* reached: if true, an exception is thrown, otherwise this
|
||||
* stream reaches its end as well (i.e. read() returns -1)
|
||||
* @throws NullPointerException if the given stream is null
|
||||
*/
|
||||
public LimitedInputStream(InputStream in, long limit, boolean prematureEndException) {
|
||||
super(in);
|
||||
if (in == null)
|
||||
throw new NullPointerException("input stream is null");
|
||||
this.limit = limit < 0 ? 0 : limit;
|
||||
this.prematureEndException = prematureEndException;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int read() throws IOException {
|
||||
int res = limit == 0 ? -1 : in.read();
|
||||
if (res < 0 && limit > 0 && prematureEndException)
|
||||
throw new IOException("unexpected end of stream");
|
||||
limit = res < 0 ? 0 : limit - 1;
|
||||
return res;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int read(byte[] b, int off, int len) throws IOException {
|
||||
int res = limit == 0 ? -1 : in.read(b, off, len > limit ? (int) limit : len);
|
||||
if (res < 0 && limit > 0 && prematureEndException)
|
||||
throw new IOException("unexpected end of stream");
|
||||
limit = res < 0 ? 0 : limit - res;
|
||||
return res;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long skip(long len) throws IOException {
|
||||
long res = in.skip(len > limit ? limit : len);
|
||||
limit -= res;
|
||||
return res;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int available() throws IOException {
|
||||
int res = in.available();
|
||||
return res > limit ? (int) limit : res;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean markSupported() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
limit = 0; // end this stream, but don't close the underlying stream
|
||||
}
|
||||
}
|
|
@ -0,0 +1,182 @@
|
|||
package io.gitlab.jfronny.commons.jlhttp.io;
|
||||
|
||||
import io.gitlab.jfronny.commons.jlhttp.JLHTTPServer;
|
||||
|
||||
import java.io.*;
|
||||
|
||||
/**
|
||||
* The {@code MultipartInputStream} decodes an InputStream whose data has
|
||||
* a "multipart/*" content type (see RFC 2046), providing the underlying
|
||||
* data of its various parts.
|
||||
* <p>
|
||||
* The {@code InputStream} methods (e.g. {@link #read}) relate only to
|
||||
* the current part, and the {@link #nextPart} method advances to the
|
||||
* beginning of the next part.
|
||||
*/
|
||||
public class MultipartInputStream extends FilterInputStream {
|
||||
|
||||
protected final byte[] boundary; // including leading CRLF--
|
||||
protected final byte[] buf = new byte[4096];
|
||||
protected int head, tail; // indices of current part's data in buf
|
||||
protected int end; // last index of input data read into buf
|
||||
protected int len; // length of found boundary
|
||||
protected int state; // initial, started data, start boundary, EOS, last boundary, epilogue
|
||||
|
||||
/**
|
||||
* Constructs a MultipartInputStream with the given underlying stream.
|
||||
*
|
||||
* @param in the underlying multipart stream
|
||||
* @param boundary the multipart boundary
|
||||
* @throws NullPointerException if the given stream or boundary is null
|
||||
* @throws IllegalArgumentException if the given boundary's size is not
|
||||
* between 1 and 70
|
||||
*/
|
||||
protected MultipartInputStream(InputStream in, byte[] boundary) {
|
||||
super(in);
|
||||
int len = boundary.length;
|
||||
if (len == 0 || len > 70)
|
||||
throw new IllegalArgumentException("invalid boundary length");
|
||||
this.boundary = new byte[len + 4]; // CRLF--boundary
|
||||
System.arraycopy(JLHTTPServer.CRLF, 0, this.boundary, 0, 2);
|
||||
this.boundary[2] = this.boundary[3] = '-';
|
||||
System.arraycopy(boundary, 0, this.boundary, 4, len);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int read() throws IOException {
|
||||
if (!fill())
|
||||
return -1;
|
||||
return buf[head++] & 0xFF;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int read(byte[] b, int off, int len) throws IOException {
|
||||
if (!fill())
|
||||
return -1;
|
||||
len = Math.min(tail - head, len);
|
||||
System.arraycopy(buf, head, b, off, len); // throws IOOBE as necessary
|
||||
head += len;
|
||||
return len;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long skip(long len) throws IOException {
|
||||
if (len <= 0 || !fill())
|
||||
return 0;
|
||||
len = Math.min(tail - head, len);
|
||||
head += len;
|
||||
return len;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int available() throws IOException {
|
||||
return tail - head;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean markSupported() {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Advances the stream position to the beginning of the next part.
|
||||
* Data read before calling this method for the first time is the preamble,
|
||||
* and data read after this method returns false is the epilogue.
|
||||
*
|
||||
* @return true if successful, or false if there are no more parts
|
||||
* @throws IOException if an error occurs
|
||||
*/
|
||||
public boolean nextPart() throws IOException {
|
||||
while (skip(buf.length) != 0) ; // skip current part (until boundary)
|
||||
head = tail += len; // the next part starts right after boundary
|
||||
state |= 1; // started data (after first boundary)
|
||||
if (state >= 8) { // found last boundary
|
||||
state |= 0x10; // now beyond last boundary (epilogue)
|
||||
return false;
|
||||
}
|
||||
findBoundary(); // update indices
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fills the buffer with more data from the underlying stream.
|
||||
*
|
||||
* @return true if there is available data for the current part,
|
||||
* or false if the current part's end has been reached
|
||||
* @throws IOException if an error occurs or the input format is invalid
|
||||
*/
|
||||
protected boolean fill() throws IOException {
|
||||
// check if we already have more available data
|
||||
if (head != tail) // remember that if we continue, head == tail below
|
||||
return true;
|
||||
// if there's no more room, shift extra unread data to beginning of buffer
|
||||
if (tail > buf.length - 256) { // max boundary + whitespace supported size
|
||||
System.arraycopy(buf, tail, buf, 0, end -= tail);
|
||||
head = tail = 0;
|
||||
}
|
||||
// read more data and look for boundary (or potential partial boundary)
|
||||
int read;
|
||||
do {
|
||||
read = super.read(buf, end, buf.length - end);
|
||||
if (read < 0)
|
||||
state |= 4; // end of stream (EOS)
|
||||
else
|
||||
end += read;
|
||||
findBoundary(); // updates tail and length to next potential boundary
|
||||
// if we found a partial boundary with no data before it, we must
|
||||
// continue reading to determine if there is more data or not
|
||||
} while (read > 0 && tail == head && len == 0);
|
||||
// update and validate state
|
||||
if (tail != 0) // anything but a boundary right at the beginning
|
||||
state |= 1; // started data (preamble or after boundary)
|
||||
if (state < 8 && len > 0)
|
||||
state |= 2; // found start boundary
|
||||
if ((state & 6) == 4 // EOS but no start boundary found
|
||||
|| len == 0 && ((state & 0xFC) == 4 // EOS but no last and no more boundaries
|
||||
|| read == 0 && tail == head)) // boundary longer than buffer
|
||||
throw new IOException("missing boundary");
|
||||
if (state >= 0x10) // in epilogue
|
||||
tail = end; // ignore boundaries, return everything
|
||||
return tail > head; // available data in current part
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds the first (potential) boundary within the buffer's remaining data.
|
||||
* Updates tail, length and state fields accordingly.
|
||||
*
|
||||
* @throws IOException if an error occurs or the input format is invalid
|
||||
*/
|
||||
protected void findBoundary() throws IOException {
|
||||
// see RFC2046#5.1.1 for boundary syntax
|
||||
len = 0;
|
||||
int off = tail - ((state & 1) != 0 || buf[0] != '-' ? 0 : 2); // skip initial CRLF?
|
||||
for (int end = this.end; tail < end; tail++, off = tail) {
|
||||
int j = tail; // end of potential boundary
|
||||
// try to match boundary value (leading CRLF is optional at first boundary)
|
||||
while (j < end && j - off < boundary.length && buf[j] == boundary[j - off])
|
||||
j++;
|
||||
// return potential partial boundary which is cut off at end of current data
|
||||
if (j + 1 >= end) // at least two more chars needed for full boundary (CRLF or --)
|
||||
return;
|
||||
// if we found the boundary value, expand selection to include full line
|
||||
if (j - off == boundary.length) {
|
||||
// check if last boundary of entire multipart
|
||||
if (buf[j] == '-' && buf[j + 1] == '-') {
|
||||
j += 2;
|
||||
state |= 8; // found last boundary that ends multipart
|
||||
}
|
||||
// allow linear whitespace after boundary
|
||||
while (j < end && (buf[j] == ' ' || buf[j] == '\t'))
|
||||
j++;
|
||||
// check for CRLF (required, except in last boundary with no epilogue)
|
||||
if (j + 1 < end && buf[j] == '\r' && buf[j + 1] == '\n') // found CRLF
|
||||
len = j - tail + 2; // including optional whitespace and CRLF
|
||||
else if (j + 1 < end || (state & 4) != 0 && j + 1 == end) // should have found or never will
|
||||
throw new IOException("boundary must end with CRLF");
|
||||
else if ((state & 4) != 0) // last boundary with no CRLF at end of data is valid
|
||||
len = j - tail;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,139 @@
|
|||
package io.gitlab.jfronny.commons.jlhttp.io;
|
||||
|
||||
import io.gitlab.jfronny.commons.jlhttp.JLHTTPServer;
|
||||
import io.gitlab.jfronny.commons.jlhttp.util.VirtualHost;
|
||||
import io.gitlab.jfronny.commons.jlhttp.api.Context;
|
||||
import io.gitlab.jfronny.commons.jlhttp.util.Headers;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* The {@code MultipartIterator} iterates over the parts of a multipart/form-data request.
|
||||
* <p>
|
||||
* For example, to support file upload from a web browser:
|
||||
* <ol>
|
||||
* <li>Create an HTML form which includes an input field of type "file", attributes
|
||||
* method="post" and enctype="multipart/form-data", and an action URL of your choice,
|
||||
* for example action="/upload". This form can be served normally like any other
|
||||
* resource, e.g. from an HTML file on disk.
|
||||
* <li>Add a context handler for the action path ("/upload" in this example), using either
|
||||
* the explicit {@link VirtualHost#addContext} method or the {@link Context} annotation.
|
||||
* <li>In the context handler implementation, construct a {@code MultipartIterator} from
|
||||
* the client {@code Request}.
|
||||
* <li>Iterate over the form {@link Part}s, processing each named field as appropriate -
|
||||
* for the file input field, read the uploaded file using the body input stream.
|
||||
* </ol>
|
||||
*/
|
||||
public class MultipartIterator implements Iterator<MultipartIterator.Part> {
|
||||
|
||||
/**
|
||||
* The {@code Part} class encapsulates a single part of the multipart.
|
||||
*/
|
||||
public static class Part {
|
||||
|
||||
public String name;
|
||||
public String filename;
|
||||
public Headers headers;
|
||||
public InputStream body;
|
||||
|
||||
/**
|
||||
* Returns the part's name (form field name).
|
||||
*
|
||||
* @return the part's name
|
||||
*/
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the part's filename (original filename entered in file form field).
|
||||
*
|
||||
* @return the part's filename, or null if there is none
|
||||
*/
|
||||
public String getFilename() {
|
||||
return filename;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the part's headers.
|
||||
*
|
||||
* @return the part's headers
|
||||
*/
|
||||
public Headers getHeaders() {
|
||||
return headers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the part's body (form field value).
|
||||
*
|
||||
* @return the part's body
|
||||
*/
|
||||
public InputStream getBody() {
|
||||
return body;
|
||||
}
|
||||
|
||||
/***
|
||||
* Returns the part's body as a string. If the part
|
||||
* headers do not specify a charset, UTF-8 is used.
|
||||
*
|
||||
* @return the part's body as a string
|
||||
* @throws IOException if an IO error occurs
|
||||
*/
|
||||
public String getString() throws IOException {
|
||||
String charset = headers.getParams("Content-Type").get("charset");
|
||||
return JLHTTPServer.readToken(body, -1, charset == null ? "UTF-8" : charset, 8192);
|
||||
}
|
||||
}
|
||||
|
||||
protected final MultipartInputStream in;
|
||||
protected boolean next;
|
||||
|
||||
/**
|
||||
* Creates a new MultipartIterator from the given request.
|
||||
*
|
||||
* @param req the multipart/form-data request
|
||||
* @throws IOException if an IO error occurs
|
||||
* @throws IllegalArgumentException if the given request's content type
|
||||
* is not multipart/form-data, or is missing the boundary
|
||||
*/
|
||||
public MultipartIterator(JLHTTPServer.Request req) throws IOException {
|
||||
Map<String, String> ct = req.getHeaders().getParams("Content-Type");
|
||||
if (!ct.containsKey("multipart/form-data"))
|
||||
throw new IllegalArgumentException("Content-Type is not multipart/form-data");
|
||||
String boundary = ct.get("boundary"); // should be US-ASCII
|
||||
if (boundary == null)
|
||||
throw new IllegalArgumentException("Content-Type is missing boundary");
|
||||
in = new MultipartInputStream(req.getBody(), JLHTTPServer.getBytes(boundary));
|
||||
}
|
||||
|
||||
public boolean hasNext() {
|
||||
try {
|
||||
return next || (next = in.nextPart());
|
||||
} catch (IOException ioe) {
|
||||
throw new RuntimeException(ioe);
|
||||
}
|
||||
}
|
||||
|
||||
public Part next() {
|
||||
if (!hasNext())
|
||||
throw new NoSuchElementException();
|
||||
next = false;
|
||||
Part p = new Part();
|
||||
try {
|
||||
p.headers = JLHTTPServer.readHeaders(in);
|
||||
} catch (IOException ioe) {
|
||||
throw new RuntimeException(ioe);
|
||||
}
|
||||
Map<String, String> cd = p.headers.getParams("Content-Disposition");
|
||||
p.name = cd.get("name");
|
||||
p.filename = cd.get("filename");
|
||||
p.body = in;
|
||||
return p;
|
||||
}
|
||||
|
||||
public void remove() {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
package io.gitlab.jfronny.commons.jlhttp.util;
|
||||
|
||||
import io.gitlab.jfronny.commons.jlhttp.JLHTTPServer;
|
||||
import io.gitlab.jfronny.commons.jlhttp.api.ContextHandler;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* The {@code FileContextHandler} services a context by mapping it
|
||||
* to a file or folder (recursively) on disk.
|
||||
*/
|
||||
public class FileContextHandler implements ContextHandler {
|
||||
|
||||
protected final File base;
|
||||
|
||||
public FileContextHandler(File dir) throws IOException {
|
||||
this.base = dir.getCanonicalFile();
|
||||
}
|
||||
|
||||
public int serve(JLHTTPServer.Request req, JLHTTPServer.Response resp) throws IOException {
|
||||
return JLHTTPServer.serveFile(base, req.getContext().getPath(), req, resp);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,45 @@
|
|||
package io.gitlab.jfronny.commons.jlhttp.util;
|
||||
|
||||
/**
|
||||
* The {@code Header} class encapsulates a single HTTP header.
|
||||
*/
|
||||
public class Header {
|
||||
|
||||
protected final String name;
|
||||
protected final String value;
|
||||
|
||||
/**
|
||||
* Constructs a header with the given name and value.
|
||||
* Leading and trailing whitespace are trimmed.
|
||||
*
|
||||
* @param name the header name
|
||||
* @param value the header value
|
||||
* @throws NullPointerException if name or value is null
|
||||
* @throws IllegalArgumentException if name is empty
|
||||
*/
|
||||
public Header(String name, String value) {
|
||||
this.name = name.trim();
|
||||
this.value = value.trim();
|
||||
// RFC2616#14.23 - header can have an empty value (e.g. Host)
|
||||
if (this.name.length() == 0) // but name cannot be empty
|
||||
throw new IllegalArgumentException("name cannot be empty");
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns this header's name.
|
||||
*
|
||||
* @return this header's name
|
||||
*/
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns this header's value.
|
||||
*
|
||||
* @return this header's value
|
||||
*/
|
||||
public String getValue() {
|
||||
return value;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,182 @@
|
|||
package io.gitlab.jfronny.commons.jlhttp.util;
|
||||
|
||||
import io.gitlab.jfronny.commons.jlhttp.JLHTTPServer;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* The {@code Headers} class encapsulates a collection of HTTP headers.
|
||||
* <p>
|
||||
* Header names are treated case-insensitively, although this class retains
|
||||
* their original case. Header insertion order is maintained as well.
|
||||
*/
|
||||
public class Headers implements Iterable<Header> {
|
||||
|
||||
// due to the requirements of case-insensitive name comparisons,
|
||||
// retaining the original case, and retaining header insertion order,
|
||||
// and due to the fact that the number of headers is generally
|
||||
// quite small (usually under 12 headers), we use a simple array with
|
||||
// linear access times, which proves to be more efficient and
|
||||
// straightforward than the alternatives
|
||||
protected Header[] headers = new Header[12];
|
||||
protected int count;
|
||||
|
||||
/**
|
||||
* Returns the number of added headers.
|
||||
*
|
||||
* @return the number of added headers
|
||||
*/
|
||||
public int size() {
|
||||
return count;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the value of the first header with the given name.
|
||||
*
|
||||
* @param name the header name (case insensitive)
|
||||
* @return the header value, or null if none exists
|
||||
*/
|
||||
public String get(String name) {
|
||||
for (int i = 0; i < count; i++)
|
||||
if (headers[i].getName().equalsIgnoreCase(name))
|
||||
return headers[i].getValue();
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the Date value of the header with the given name.
|
||||
*
|
||||
* @param name the header name (case insensitive)
|
||||
* @return the header value as a Date, or null if none exists
|
||||
* or if the value is not in any supported date format
|
||||
*/
|
||||
public Date getDate(String name) {
|
||||
try {
|
||||
String header = get(name);
|
||||
return header == null ? null : JLHTTPServer.parseDate(header);
|
||||
} catch (IllegalArgumentException iae) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether there exists a header with the given name.
|
||||
*
|
||||
* @param name the header name (case insensitive)
|
||||
* @return whether there exists a header with the given name
|
||||
*/
|
||||
public boolean contains(String name) {
|
||||
return get(name) != null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a header with the given name and value to the end of this
|
||||
* collection of headers. Leading and trailing whitespace are trimmed.
|
||||
*
|
||||
* @param name the header name (case insensitive)
|
||||
* @param value the header value
|
||||
*/
|
||||
public void add(String name, String value) {
|
||||
Header header = new Header(name, value); // also validates
|
||||
// expand array if necessary
|
||||
if (count == headers.length) {
|
||||
Header[] expanded = new Header[2 * count];
|
||||
System.arraycopy(headers, 0, expanded, 0, count);
|
||||
headers = expanded;
|
||||
}
|
||||
headers[count++] = header; // inlining header would cause a bug!
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds all given headers to the end of this collection of headers,
|
||||
* in their original order.
|
||||
*
|
||||
* @param headers the headers to add
|
||||
*/
|
||||
public void addAll(Headers headers) {
|
||||
for (Header header : headers)
|
||||
add(header.getName(), header.getValue());
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a header with the given name and value, replacing the first
|
||||
* existing header with the same name. If there is no existing header
|
||||
* with the same name, it is added as in {@link #add}.
|
||||
*
|
||||
* @param name the header name (case insensitive)
|
||||
* @param value the header value
|
||||
* @return the replaced header, or null if none existed
|
||||
*/
|
||||
public Header replace(String name, String value) {
|
||||
for (int i = 0; i < count; i++) {
|
||||
if (headers[i].getName().equalsIgnoreCase(name)) {
|
||||
Header prev = headers[i];
|
||||
headers[i] = new Header(name, value);
|
||||
return prev;
|
||||
}
|
||||
}
|
||||
add(name, value);
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes all headers with the given name (if any exist).
|
||||
*
|
||||
* @param name the header name (case insensitive)
|
||||
*/
|
||||
public void remove(String name) {
|
||||
int j = 0;
|
||||
for (int i = 0; i < count; i++)
|
||||
if (!headers[i].getName().equalsIgnoreCase(name))
|
||||
headers[j++] = headers[i];
|
||||
while (count > j)
|
||||
headers[--count] = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes the headers to the given stream (including trailing CRLF).
|
||||
*
|
||||
* @param out the stream to write the headers to
|
||||
* @throws IOException if an error occurs
|
||||
*/
|
||||
public void writeTo(OutputStream out) throws IOException {
|
||||
for (int i = 0; i < count; i++) {
|
||||
out.write(JLHTTPServer.getBytes(headers[i].getName(), ": ", headers[i].getValue()));
|
||||
out.write(JLHTTPServer.CRLF);
|
||||
}
|
||||
out.write(JLHTTPServer.CRLF); // ends header block
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a header's parameters. Parameter order is maintained,
|
||||
* and the first key (in iteration order) is the header's value
|
||||
* without the parameters.
|
||||
*
|
||||
* @param name the header name (case insensitive)
|
||||
* @return the header's parameter names and values
|
||||
*/
|
||||
public Map<String, String> getParams(String name) {
|
||||
Map<String, String> params = new LinkedHashMap<String, String>();
|
||||
for (String param : JLHTTPServer.split(get(name), ";", -1)) {
|
||||
String[] pair = JLHTTPServer.split(param, "=", 2);
|
||||
String val = pair.length == 1 ? "" : JLHTTPServer.trimLeft(JLHTTPServer.trimRight(pair[1], '"'), '"');
|
||||
params.put(pair[0], val);
|
||||
}
|
||||
return params;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an iterator over the headers, in their insertion order.
|
||||
* If the headers collection is modified during iteration, the
|
||||
* iteration result is undefined. The remove operation is unsupported.
|
||||
*
|
||||
* @return an Iterator over the headers
|
||||
*/
|
||||
public Iterator<Header> iterator() {
|
||||
// we use the built-in wrapper instead of a trivial custom implementation
|
||||
// since even a tiny anonymous class here compiles to a 1.5K class file
|
||||
return Arrays.asList(headers).subList(0, count).iterator();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,44 @@
|
|||
package io.gitlab.jfronny.commons.jlhttp.util;
|
||||
|
||||
import io.gitlab.jfronny.commons.jlhttp.JLHTTPServer;
|
||||
import io.gitlab.jfronny.commons.jlhttp.api.ContextHandler;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.lang.reflect.Method;
|
||||
|
||||
/**
|
||||
* The {@code MethodContextHandler} services a context
|
||||
* by invoking a handler method on a specified object.
|
||||
* <p>
|
||||
* The method must have the same signature and contract as
|
||||
* {@link ContextHandler#serve}, but can have an arbitrary name.
|
||||
*
|
||||
* @see VirtualHost#addContexts(Object)
|
||||
*/
|
||||
public class MethodContextHandler implements ContextHandler {
|
||||
|
||||
protected final Method m;
|
||||
protected final Object obj;
|
||||
|
||||
public MethodContextHandler(Method m, Object obj) throws IllegalArgumentException {
|
||||
this.m = m;
|
||||
this.obj = obj;
|
||||
Class<?>[] params = m.getParameterTypes();
|
||||
if (params.length != 2
|
||||
|| !JLHTTPServer.Request.class.isAssignableFrom(params[0])
|
||||
|| !JLHTTPServer.Response.class.isAssignableFrom(params[1])
|
||||
|| !int.class.isAssignableFrom(m.getReturnType()))
|
||||
throw new IllegalArgumentException("invalid method signature: " + m);
|
||||
}
|
||||
|
||||
public int serve(JLHTTPServer.Request req, JLHTTPServer.Response resp) throws IOException {
|
||||
try {
|
||||
return (Integer) m.invoke(obj, req, resp);
|
||||
} catch (InvocationTargetException ite) {
|
||||
throw new IOException("error: " + ite.getCause().getMessage());
|
||||
} catch (Exception e) {
|
||||
throw new IOException("error: " + e);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,228 @@
|
|||
package io.gitlab.jfronny.commons.jlhttp.util;
|
||||
|
||||
import io.gitlab.jfronny.commons.jlhttp.JLHTTPServer;
|
||||
import io.gitlab.jfronny.commons.jlhttp.api.Context;
|
||||
import io.gitlab.jfronny.commons.jlhttp.api.ContextHandler;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.*;
|
||||
|
||||
/**
|
||||
* The {@code VirtualHost} class represents a virtual host in the server.
|
||||
*/
|
||||
public class VirtualHost {
|
||||
|
||||
/**
|
||||
* The {@code ContextInfo} class holds a single context's information.
|
||||
*/
|
||||
public class ContextInfo {
|
||||
|
||||
protected final String path;
|
||||
protected final Map<String, ContextHandler> handlers =
|
||||
new ConcurrentHashMap<>(2);
|
||||
|
||||
/**
|
||||
* Constructs a ContextInfo with the given context path.
|
||||
*
|
||||
* @param path the context path (without trailing slash)
|
||||
*/
|
||||
public ContextInfo(String path) {
|
||||
this.path = path;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the context path.
|
||||
*
|
||||
* @return the context path, or null if there is none
|
||||
*/
|
||||
public String getPath() {
|
||||
return path;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the map of supported HTTP methods and their corresponding handlers.
|
||||
*
|
||||
* @return the map of supported HTTP methods and their corresponding handlers
|
||||
*/
|
||||
public Map<String, ContextHandler> getHandlers() {
|
||||
return handlers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds (or replaces) a context handler for the given HTTP methods.
|
||||
*
|
||||
* @param handler the context handler
|
||||
* @param methods the HTTP methods supported by the handler (default is "GET")
|
||||
*/
|
||||
public void addHandler(ContextHandler handler, String... methods) {
|
||||
if (methods.length == 0)
|
||||
methods = new String[]{"GET"};
|
||||
for (String method : methods) {
|
||||
handlers.put(method, handler);
|
||||
VirtualHost.this.methods.add(method); // it's now supported by server
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected final String name;
|
||||
protected final Set<String> aliases = new CopyOnWriteArraySet<>();
|
||||
protected volatile String directoryIndex = "index.html";
|
||||
protected volatile boolean allowGeneratedIndex;
|
||||
protected final Set<String> methods = new CopyOnWriteArraySet<>();
|
||||
protected final ContextInfo emptyContext = new ContextInfo(null);
|
||||
protected final ConcurrentMap<String, ContextInfo> contexts =
|
||||
new ConcurrentHashMap<>();
|
||||
|
||||
/**
|
||||
* Constructs a VirtualHost with the given name.
|
||||
*
|
||||
* @param name the host's name, or null if it is the default host
|
||||
*/
|
||||
public VirtualHost(String name) {
|
||||
this.name = name;
|
||||
contexts.put("*", new ContextInfo(null)); // for "OPTIONS *"
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns this host's name.
|
||||
*
|
||||
* @return this host's name, or null if it is the default host
|
||||
*/
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds an alias for this host.
|
||||
*
|
||||
* @param alias the alias
|
||||
*/
|
||||
public void addAlias(String alias) {
|
||||
aliases.add(alias);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns this host's aliases.
|
||||
*
|
||||
* @return the (unmodifiable) set of aliases (which may be empty)
|
||||
*/
|
||||
public Set<String> getAliases() {
|
||||
return Collections.unmodifiableSet(aliases);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the directory index file. For every request whose URI ends with
|
||||
* a '/' (i.e. a directory), the index file is appended to the path,
|
||||
* and the resulting resource is served if it exists. If it does not
|
||||
* exist, an auto-generated index for the requested directory may be
|
||||
* served, depending on whether {@link #setAllowGeneratedIndex
|
||||
* a generated index is allowed}, otherwise an error is returned.
|
||||
* The default directory index file is "index.html".
|
||||
*
|
||||
* @param directoryIndex the directory index file, or null if no
|
||||
* index file should be used
|
||||
*/
|
||||
public void setDirectoryIndex(String directoryIndex) {
|
||||
this.directoryIndex = directoryIndex;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets this host's directory index file.
|
||||
*
|
||||
* @return the directory index file, or null
|
||||
*/
|
||||
public String getDirectoryIndex() {
|
||||
return directoryIndex;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets whether auto-generated indices are allowed. If false, and a
|
||||
* directory resource is requested, an error will be returned instead.
|
||||
*
|
||||
* @param allowed specifies whether generated indices are allowed
|
||||
*/
|
||||
public void setAllowGeneratedIndex(boolean allowed) {
|
||||
this.allowGeneratedIndex = allowed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether auto-generated indices are allowed.
|
||||
*
|
||||
* @return whether auto-generated indices are allowed
|
||||
*/
|
||||
public boolean isAllowGeneratedIndex() {
|
||||
return allowGeneratedIndex;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all HTTP methods explicitly supported by at least one context
|
||||
* (this may or may not include the methods with required or built-in support).
|
||||
*
|
||||
* @return all HTTP methods explicitly supported by at least one context
|
||||
*/
|
||||
public Set<String> getMethods() {
|
||||
return methods;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the context handler for the given path.
|
||||
* <p>
|
||||
* If a context is not found for the given path, the search is repeated for
|
||||
* its parent path, and so on until a base context is found. If neither the
|
||||
* given path nor any of its parents has a context, an empty context is returned.
|
||||
*
|
||||
* @param path the context's path
|
||||
* @return the context info for the given path, or an empty context if none exists
|
||||
*/
|
||||
public ContextInfo getContext(String path) {
|
||||
// all context paths are without trailing slash
|
||||
for (path = JLHTTPServer.trimRight(path, '/'); path != null; path = JLHTTPServer.getParentPath(path)) {
|
||||
ContextInfo info = contexts.get(path);
|
||||
if (info != null)
|
||||
return info;
|
||||
}
|
||||
return emptyContext;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a context and its corresponding context handler to this server.
|
||||
* Paths are normalized by removing trailing slashes (except the root).
|
||||
*
|
||||
* @param path the context's path (must start with '/')
|
||||
* @param handler the context handler for the given path
|
||||
* @param methods the HTTP methods supported by the context handler (default is "GET")
|
||||
* @throws IllegalArgumentException if path is malformed
|
||||
*/
|
||||
public void addContext(String path, ContextHandler handler, String... methods) {
|
||||
if (path == null || !path.startsWith("/") && !path.equals("*"))
|
||||
throw new IllegalArgumentException("invalid path: " + path);
|
||||
path = JLHTTPServer.trimRight(path, '/'); // remove trailing slash
|
||||
ContextInfo info = new ContextInfo(path);
|
||||
ContextInfo existing = contexts.putIfAbsent(path, info);
|
||||
info = existing != null ? existing : info;
|
||||
info.addHandler(handler, methods);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds contexts for all methods of the given object that
|
||||
* are annotated with the {@link Context} annotation.
|
||||
*
|
||||
* @param o the object whose annotated methods are added
|
||||
* @throws IllegalArgumentException if a Context-annotated
|
||||
* method has an {@link Context invalid signature}
|
||||
*/
|
||||
public void addContexts(Object o) throws IllegalArgumentException {
|
||||
for (Class<?> c = o.getClass(); c != null; c = c.getSuperclass()) {
|
||||
// add to contexts those with @Context annotation
|
||||
for (Method m : c.getDeclaredMethods()) {
|
||||
Context context = m.getAnnotation(Context.class);
|
||||
if (context != null) {
|
||||
m.setAccessible(true); // allow access to private method
|
||||
ContextHandler handler = new MethodContextHandler(m, o);
|
||||
addContext(context.value(), handler, context.methods());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
module io.gitlab.jfronny.commons.jlhttp {
|
||||
exports io.gitlab.jfronny.commons.jlhttp;
|
||||
exports io.gitlab.jfronny.commons.jlhttp.api;
|
||||
exports io.gitlab.jfronny.commons.jlhttp.io;
|
||||
exports io.gitlab.jfronny.commons.jlhttp.util;
|
||||
}
|
|
@ -1,6 +1,7 @@
|
|||
rootProject.name = "Commons"
|
||||
|
||||
include("commons-gson")
|
||||
include("commons-jlhttp")
|
||||
include("commons-manifold")
|
||||
include("commons-slf4j")
|
||||
include("muscript")
|
||||
|
|
Loading…
Reference in New Issue