1
0
mirror of https://github.com/octoleo/plantuml.git synced 2024-12-22 10:59:01 +00:00

Add "POST /render" endpoint to picoweb server.

It expects a JSON body like
  { "source": PLANTUML_SOURCE_STRING, "options": ARRAY_OF_STRINGS }

And returns the rendered diagram with suitable Content-Type header (all output formats are supported).
This commit is contained in:
matthew16550 2021-02-13 01:09:28 +11:00
parent 7b784cd2f6
commit a4553adeb2
7 changed files with 646 additions and 57 deletions

View File

@ -60,8 +60,37 @@ import net.sourceforge.plantuml.ugraphic.UFont;
* *
*/ */
public enum FileFormat { public enum FileFormat {
PNG, SVG, EPS, EPS_TEXT, ATXT, UTXT, XMI_STANDARD, XMI_STAR, XMI_ARGO, SCXML, PDF, MJPEG, ANIMATED_GIF, HTML, HTML5, PNG("image/png"),
VDX, LATEX, LATEX_NO_PREAMBLE, BASE64, BRAILLE_PNG, PREPROC; SVG("image/svg+xml"),
EPS("application/postscript"),
EPS_TEXT("application/postscript"),
ATXT("text/plain"),
UTXT("text/plain;charset=UTF-8"),
XMI_STANDARD("application/vnd.xmi+xml"),
XMI_STAR("application/vnd.xmi+xml"),
XMI_ARGO("application/vnd.xmi+xml"),
SCXML("application/scxml+xml"),
PDF("application/pdf"),
MJPEG("video/x-msvideo"),
ANIMATED_GIF("image/gif"),
HTML("text/html"),
HTML5("text/html"),
VDX("application/vnd.visio.xml"),
LATEX("application/x-latex"),
LATEX_NO_PREAMBLE("application/x-latex"),
BASE64("text/plain; charset=x-user-defined"),
BRAILLE_PNG("image/png"),
PREPROC("text/plain");
private final String mimeType;
FileFormat(String mimeType) {
this.mimeType = mimeType;
}
public String getMimeType() {
return mimeType;
}
/** /**
* Returns the file format to be used for that format. * Returns the file format to be used for that format.

View File

@ -225,7 +225,7 @@ public class SourceStringReader {
} }
private void noStartumlFound(OutputStream os, FileFormatOption fileFormatOption, long seed) throws IOException { public ImageData noStartumlFound(OutputStream os, FileFormatOption fileFormatOption, long seed) throws IOException {
final TextBlockBackcolored error = GraphicStrings.createForError(Arrays.asList("No @startuml/@enduml found"), final TextBlockBackcolored error = GraphicStrings.createForError(Arrays.asList("No @startuml/@enduml found"),
fileFormatOption.isUseRedForError()); fileFormatOption.isUseRedForError());
HColor backcolor = error.getBackcolor(); HColor backcolor = error.getBackcolor();
@ -233,7 +233,7 @@ public class SourceStringReader {
null, ClockwiseTopRightBottomLeft.none(), backcolor); null, ClockwiseTopRightBottomLeft.none(), backcolor);
final ImageBuilder imageBuilder = ImageBuilder.build(imageParameter); final ImageBuilder imageBuilder = ImageBuilder.build(imageParameter);
imageBuilder.setUDrawable(error); imageBuilder.setUDrawable(error);
imageBuilder.writeImageTOBEMOVED(fileFormatOption, seed, os); return imageBuilder.writeImageTOBEMOVED(fileFormatOption, seed, os);
} }
public final List<BlockUml> getBlocks() { public final List<BlockUml> getBlocks() {

View File

@ -0,0 +1,14 @@
package net.sourceforge.plantuml.picoweb;
import java.io.IOException;
public class BadRequest400 extends IOException {
public BadRequest400(String message) {
super(message);
}
public BadRequest400(String message, Throwable cause) {
super(message, cause);
}
}

View File

@ -36,34 +36,40 @@
*/ */
package net.sourceforge.plantuml.picoweb; package net.sourceforge.plantuml.picoweb;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream; import java.io.BufferedOutputStream;
import java.io.BufferedReader;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
import java.io.IOException; import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStream; import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.InetAddress; import java.net.InetAddress;
import java.net.ServerSocket; import java.net.ServerSocket;
import java.net.Socket; import java.net.Socket;
import java.util.Collections;
import java.util.Date; import java.util.Date;
import java.util.List; import java.util.List;
import java.util.StringTokenizer;
import net.sourceforge.plantuml.BlockUml; import net.sourceforge.plantuml.BlockUml;
import net.sourceforge.plantuml.ErrorUml; import net.sourceforge.plantuml.ErrorUml;
import net.sourceforge.plantuml.FileFormat; import net.sourceforge.plantuml.FileFormat;
import net.sourceforge.plantuml.FileFormatOption; import net.sourceforge.plantuml.FileFormatOption;
import net.sourceforge.plantuml.LineLocationImpl;
import net.sourceforge.plantuml.Option;
import net.sourceforge.plantuml.SourceStringReader; import net.sourceforge.plantuml.SourceStringReader;
import net.sourceforge.plantuml.StringLocated;
import net.sourceforge.plantuml.StringUtils; import net.sourceforge.plantuml.StringUtils;
import net.sourceforge.plantuml.code.NoPlantumlCompressionException;
import net.sourceforge.plantuml.code.Transcoder; import net.sourceforge.plantuml.code.Transcoder;
import net.sourceforge.plantuml.code.TranscoderUtil; import net.sourceforge.plantuml.code.TranscoderUtil;
import net.sourceforge.plantuml.core.Diagram; import net.sourceforge.plantuml.core.Diagram;
import net.sourceforge.plantuml.core.ImageData; import net.sourceforge.plantuml.core.ImageData;
import net.sourceforge.plantuml.error.PSystemError; import net.sourceforge.plantuml.error.PSystemError;
import net.sourceforge.plantuml.error.PSystemErrorUtils;
import net.sourceforge.plantuml.graphic.QuoteUtils; import net.sourceforge.plantuml.graphic.QuoteUtils;
import net.sourceforge.plantuml.version.Version; import net.sourceforge.plantuml.version.Version;
import static java.nio.charset.StandardCharsets.UTF_8;
import static net.sourceforge.plantuml.ErrorUmlType.SYNTAX_ERROR;
public class PicoWebServer implements Runnable { public class PicoWebServer implements Runnable {
private final Socket connect; private final Socket connect;
@ -80,6 +86,10 @@ public class PicoWebServer implements Runnable {
final InetAddress bindAddress1 = bindAddress == null ? null : InetAddress.getByName(bindAddress); final InetAddress bindAddress1 = bindAddress == null ? null : InetAddress.getByName(bindAddress);
final ServerSocket serverConnect = new ServerSocket(port, 50, bindAddress1); final ServerSocket serverConnect = new ServerSocket(port, 50, bindAddress1);
System.err.println("webPort=" + serverConnect.getLocalPort()); System.err.println("webPort=" + serverConnect.getLocalPort());
serverLoop(serverConnect);
}
public static void serverLoop(final ServerSocket serverConnect) throws IOException {
while (true) { while (true) {
final PicoWebServer myServer = new PicoWebServer(serverConnect.accept()); final PicoWebServer myServer = new PicoWebServer(serverConnect.accept());
final Thread thread = new Thread(myServer); final Thread thread = new Thread(myServer);
@ -88,30 +98,25 @@ public class PicoWebServer implements Runnable {
} }
public void run() { public void run() {
BufferedReader in = null; BufferedInputStream in = null;
BufferedOutputStream out = null; BufferedOutputStream out = null;
try { try {
in = new BufferedReader(new InputStreamReader(connect.getInputStream(), "UTF-8")); in = new BufferedInputStream(connect.getInputStream());
out = new BufferedOutputStream(connect.getOutputStream()); out = new BufferedOutputStream(connect.getOutputStream());
final String first = in.readLine(); final ReceivedHTTPRequest request = ReceivedHTTPRequest.fromStream(in);
if (first == null) { if (request.getMethod().equals("GET")) {
if (request.getPath().startsWith("/png/") && handleGET(request, out, FileFormat.PNG))
return; return;
} if (request.getPath().startsWith("/plantuml/png/") && handleGET(request, out, FileFormat.PNG))
final StringTokenizer parse = new StringTokenizer(first);
final String method = parse.nextToken().toUpperCase();
if (method.equals("GET")) {
final String path = parse.nextToken();
if (path.startsWith("/png/") && sendDiagram(out, path, "image/png", FileFormat.PNG))
return; return;
if (path.startsWith("/plantuml/png/") && sendDiagram(out, path, "image/png", FileFormat.PNG)) if (request.getPath().startsWith("/svg/") && handleGET(request, out, FileFormat.SVG))
return; return;
if (path.startsWith("/svg/") && sendDiagram(out, path, "image/svg+xml", FileFormat.SVG)) if (request.getPath().startsWith("/plantuml/svg/") && handleGET(request, out, FileFormat.SVG))
return; return;
if (path.startsWith("/plantuml/svg/") && sendDiagram(out, path, "image/svg+xml", FileFormat.SVG)) } else if (request.getMethod().equals("POST") && request.getPath().equals("/render")) {
handleRenderRequest(request, out);
return; return;
} }
write(out, "HTTP/1.1 302 Found"); write(out, "HTTP/1.1 302 Found");
@ -120,7 +125,12 @@ public class PicoWebServer implements Runnable {
out.flush(); out.flush();
} catch (Throwable e) { } catch (Throwable e) {
try {
sendError(e, out);
}
catch (Throwable e1) {
e.printStackTrace(); e.printStackTrace();
}
} finally { } finally {
try { try {
in.close(); in.close();
@ -132,26 +142,74 @@ public class PicoWebServer implements Runnable {
} }
} }
private boolean sendDiagram(BufferedOutputStream out, String path, final String mime, final FileFormat format) private boolean handleGET(ReceivedHTTPRequest request, BufferedOutputStream out, final FileFormat format) throws IOException {
throws NoPlantumlCompressionException, IOException { final int x = request.getPath().lastIndexOf('/');
final int x = path.lastIndexOf('/'); final String compressed = request.getPath().substring(x + 1);
final String compressed = path.substring(x + 1);
final Transcoder transcoder = TranscoderUtil.getDefaultTranscoderProtected(); final Transcoder transcoder = TranscoderUtil.getDefaultTranscoderProtected();
final String source = transcoder.decode(compressed); final String source = transcoder.decode(compressed);
final SourceStringReader ssr = new SourceStringReader(source); final SourceStringReader ssr = new SourceStringReader(source);
final List<BlockUml> blocks = ssr.getBlocks(); final List<BlockUml> blocks = ssr.getBlocks();
if (blocks.size() > 0) { if (blocks.size() > 0) {
final FileFormatOption fileFormatOption = new FileFormatOption(format);
final Diagram system = blocks.get(0).getDiagram(); final Diagram system = blocks.get(0).getDiagram();
final ByteArrayOutputStream os = new ByteArrayOutputStream(); final ByteArrayOutputStream os = new ByteArrayOutputStream();
final ImageData imageData = system.exportDiagram(os, 0, new FileFormatOption(format)); final ImageData imageData = system.exportDiagram(os, 0, fileFormatOption);
os.close(); os.close();
final byte[] fileData = os.toByteArray(); sendDiagram(out, system, fileFormatOption, httpReturnCode(imageData.getStatus()), imageData, os.toByteArray());
write(out, "HTTP/1.1 " + httpReturnCode(imageData.getStatus())); return true;
}
return false;
}
private void handleRenderRequest(ReceivedHTTPRequest request, BufferedOutputStream out) throws Exception {
if (request.getBody().length == 0) {
throw new BadRequest400("No request body");
}
final RenderRequest renderRequest;
try {
renderRequest = RenderRequest.fromJson(new String(request.getBody(), UTF_8));
} catch (Exception e) {
throw new BadRequest400("Error parsing request json: " + e.getMessage(), e);
}
final Option option = new Option(renderRequest.getOptions());
final String source = renderRequest.getSource().startsWith("@start")
? renderRequest.getSource()
: "@startuml\n" + renderRequest.getSource() + "\n@enduml";
final SourceStringReader ssr = new SourceStringReader(option.getDefaultDefines(), source, option.getConfig());
final ByteArrayOutputStream os = new ByteArrayOutputStream();
final Diagram system;
final ImageData imageData;
if (ssr.getBlocks().size() == 0) {
system = PSystemErrorUtils.buildV2(
null,
new ErrorUml(SYNTAX_ERROR, "No @startuml/@enduml found", 0, new LineLocationImpl("", null)),
null,
Collections.<StringLocated>emptyList()
);
imageData = ssr.noStartumlFound(os, option.getFileFormatOption(),42);
} else {
system = ssr.getBlocks().get(0).getDiagram();
imageData = system.exportDiagram(os, 0, option.getFileFormatOption());
}
sendDiagram(out, system, option.getFileFormatOption(), "200", imageData, os.toByteArray());
}
private void sendDiagram(final BufferedOutputStream out, final Diagram system, final FileFormatOption fileFormatOption,
final String returnCode, final ImageData imageData, final byte[] fileData)
throws IOException {
write(out, "HTTP/1.1 " + returnCode);
write(out, "Cache-Control: no-cache"); write(out, "Cache-Control: no-cache");
write(out, "Server: PlantUML PicoWebServer " + Version.versionString()); write(out, "Server: PlantUML PicoWebServer " + Version.versionString());
write(out, "Date: " + new Date()); write(out, "Date: " + new Date());
write(out, "Content-type: " + mime); write(out, "Content-type: " + fileFormatOption.getFileFormat().getMimeType());
write(out, "Content-length: " + fileData.length); write(out, "Content-length: " + fileData.length);
write(out, "X-PlantUML-Diagram-Width: " + imageData.getWidth()); write(out, "X-PlantUML-Diagram-Width: " + imageData.getWidth());
write(out, "X-PlantUML-Diagram-Height: " + imageData.getHeight()); write(out, "X-PlantUML-Diagram-Height: " + imageData.getHeight());
@ -170,9 +228,25 @@ public class PicoWebServer implements Runnable {
out.flush(); out.flush();
out.write(fileData); out.write(fileData);
out.flush(); out.flush();
return true;
} }
return false;
private void sendError(Throwable e, BufferedOutputStream out) throws Exception {
final ByteArrayOutputStream baos = new ByteArrayOutputStream();
final PrintWriter printWriter = new PrintWriter(baos);
if (e instanceof BadRequest400 && e.getCause() == null) {
printWriter.write(e.getMessage());
} else {
e.printStackTrace(printWriter);
}
printWriter.close();
write(out, "HTTP/1.1 " + (e instanceof BadRequest400 ? "400 Bad Request" : "500 Internal Server Error"));
write(out, "Content-type: text/plain");
write(out, "Content-length: " + baos.size());
write(out, "");
out.write(baos.toByteArray());
out.flush();
} }
private String httpReturnCode(int status) { private String httpReturnCode(int status) {

View File

@ -0,0 +1,316 @@
package net.sourceforge.plantuml.picoweb;
import net.sourceforge.plantuml.json.Json;
import net.sourceforge.plantuml.json.JsonObject;
import javax.imageio.ImageIO;
import javax.imageio.stream.MemoryCacheImageInputStream;
import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.URL;
import static java.nio.charset.StandardCharsets.UTF_8;
import static net.sourceforge.plantuml.code.TranscoderUtil.getDefaultTranscoder;
// Newer Java versions have nice built-in HTTP classes in the jdk.incubator.httpclient / java.net.http packages
// but PlantUML supports older Java versions so the tests here use a kludgy approach to HTTP.
// Multi-line strings here start with a "" so that IDE auto-indenting will leave the rest of the string nicely
// aligned
public class PicoWebServerTest {
public static void main(String[] args) throws Exception {
startServer();
test_basic_http();
test_GET_png();
test_GET_svg();
test_POST_render();
test_unknown_path();
}
//
// Test Cases
//
private static void test_basic_http() throws Exception {
assert httpRaw(
""
).equals("" +
"HTTP/1.1 400 Bad Request\n" +
"Content-type: text/plain\n" +
"Content-length: 16\n" +
"\n" +
"Bad request line"
);
assert httpRaw(
"GET"
).equals("" +
"HTTP/1.1 400 Bad Request\n" +
"Content-type: text/plain\n" +
"Content-length: 16\n" +
"\n" +
"Bad request line"
);
assert httpRaw("" +
"GET /foo HTTP/1.1\n" +
"Content-Length: bar\n"
).equals("" +
"HTTP/1.1 400 Bad Request\n" +
"Content-type: text/plain\n" +
"Content-length: 22\n" +
"\n" +
"Invalid content length"
);
assert httpRaw("" +
"GET /foo HTTP/1.1\n" +
"Content-Length: -1\n"
).equals("" +
"HTTP/1.1 400 Bad Request\n" +
"Content-type: text/plain\n" +
"Content-length: 23\n" +
"\n" +
"Negative content length"
);
assert httpRaw("" +
"GET /foo HTTP/1.1\n" +
"Content-Length: 3\n" +
"\n" +
"12"
).equals("" +
"HTTP/1.1 400 Bad Request\n" +
"Content-type: text/plain\n" +
"Content-length: 14\n" +
"\n" +
"Body too short"
);
}
private static void test_GET_png() throws Exception {
HttpURLConnection response;
response = httpGet("/png/" + getDefaultTranscoder().encode("A -> B"));
assert response.getResponseCode() == 200;
assert response.getHeaderField("X-PlantUML-Diagram-Error") == null;
assert response.getHeaderField("X-PlantUML-Diagram-Error-Line") == null;
assert response.getContentType().equals("image/png");
assert readStreamAsImage(response.getInputStream()) != null;
response = httpGet("/plantuml/png/" + getDefaultTranscoder().encode("A -> B"));
assert response.getResponseCode() == 200;
assert response.getHeaderField("X-PlantUML-Diagram-Error") == null;
assert response.getHeaderField("X-PlantUML-Diagram-Error-Line") == null;
assert response.getContentType().equals("image/png");
assert readStreamAsImage(response.getInputStream()) != null;
response = httpGet("/png/" + getDefaultTranscoder().encode("foo"));
assert response.getResponseCode() == 400;
assert response.getHeaderField("X-PlantUML-Diagram-Error").equals("Syntax Error?");
assert response.getHeaderField("X-PlantUML-Diagram-Error-Line").equals("2");
assert response.getContentType().equals("image/png");
assert readStreamAsImage(response.getErrorStream()) != null;
}
private static void test_GET_svg() throws Exception {
HttpURLConnection response;
response = httpGet("/svg/" + getDefaultTranscoder().encode("A -> B"));
assert response.getResponseCode() == 200;
assert response.getHeaderField("X-PlantUML-Diagram-Error") == null;
assert response.getHeaderField("X-PlantUML-Diagram-Error-Line") == null;
assert response.getContentType().equals("image/svg+xml");
assert readStreamAsString(response.getInputStream()).startsWith("<?xml ");
response = httpGet("/plantuml/svg/" + getDefaultTranscoder().encode("A -> B"));
assert response.getResponseCode() == 200;
assert response.getHeaderField("X-PlantUML-Diagram-Error") == null;
assert response.getHeaderField("X-PlantUML-Diagram-Error-Line") == null;
assert response.getContentType().equals("image/svg+xml");
assert readStreamAsString(response.getInputStream()).startsWith("<?xml ");
response = httpGet("/svg/" + getDefaultTranscoder().encode("foo"));
assert response.getResponseCode() == 400;
assert response.getHeaderField("X-PlantUML-Diagram-Error").equals("Syntax Error?");
assert response.getHeaderField("X-PlantUML-Diagram-Error-Line").equals("2");
assert response.getContentType().equals("image/svg+xml");
assert readStreamAsString(response.getErrorStream()).startsWith("<?xml ");
}
private static void test_POST_render() throws Exception {
HttpURLConnection response;
// Defaults to png when no format is specified
response = httpPostJson("/render", renderRequestJson("A -> B"));
assert response.getResponseCode() == 200;
assert response.getHeaderField("X-PlantUML-Diagram-Error") == null;
assert response.getHeaderField("X-PlantUML-Diagram-Error-Line") == null;
assert response.getContentType().equals("image/png");
assert readStreamAsImage(response.getInputStream()) != null;
response = httpPostJson("/render", renderRequestJson("A -> B", "-tsvg"));
assert response.getResponseCode() == 200;
assert response.getHeaderField("X-PlantUML-Diagram-Error") == null;
assert response.getHeaderField("X-PlantUML-Diagram-Error-Line") == null;
assert response.getContentType().equals("image/svg+xml");
assert readStreamAsString(response.getInputStream()).startsWith("<?xml ");
response = httpPostJson("/render", renderRequestJson("A -> B", "-ttxt"));
assert response.getResponseCode() == 200;
assert response.getHeaderField("X-PlantUML-Diagram-Error") == null;
assert response.getHeaderField("X-PlantUML-Diagram-Error-Line") == null;
assert response.getContentType().equals("text/plain");
assert readStreamAsString(response.getInputStream()).equals("" +
" ,-. ,-.\n" +
" |A| |B|\n" +
" `+' `+'\n" +
" | | \n" +
" |----------->| \n" +
" ,+. ,+.\n" +
" |A| |B|\n" +
" `-' `-'\n"
);
response = httpPostJson("/render", renderRequestJson("foo", "-ttxt"));
assert response.getResponseCode() == 200;
assert response.getHeaderField("X-PlantUML-Diagram-Error").equals("Syntax Error?");
assert response.getHeaderField("X-PlantUML-Diagram-Error-Line").equals("2");
assert response.getContentType().equals("text/plain");
assert readStreamAsString(response.getInputStream()).equals("" +
"[From string (line 2) ]\n" +
" \n" +
"@startuml \n" +
"foo \n" +
"^^^^^ \n" +
" Syntax Error? \n"
);
response = httpPostJson("/render", renderRequestJson("@startuml", "-ttxt"));
assert response.getResponseCode() == 200;
assert response.getHeaderField("X-PlantUML-Diagram-Error").equals("No @startuml/@enduml found");
assert response.getHeaderField("X-PlantUML-Diagram-Error-Line").equals("0");
assert response.getContentType().equals("text/plain");
assert readStreamAsString(response.getInputStream()).equals("" +
" \n" +
" \n" +
" No @startuml/@enduml found\n"
);
response = httpPostJson("/render", "");
assert response.getResponseCode() == 400;
assert response.getHeaderField("X-PlantUML-Diagram-Error") == null;
assert response.getHeaderField("X-PlantUML-Diagram-Error-Line") == null;
assert response.getContentType().equals("text/plain");
assert readStreamAsString(response.getErrorStream()).equals("No request body");
response = httpPostJson("/render", "123abc");
assert response.getResponseCode() == 400;
assert response.getHeaderField("X-PlantUML-Diagram-Error") == null;
assert response.getHeaderField("X-PlantUML-Diagram-Error-Line") == null;
assert response.getContentType().equals("text/plain");
assert readStreamAsString(response.getErrorStream()).contains("Error parsing request json: Unexpected character at 1:4\n");
}
private static void test_unknown_path() throws Exception {
HttpURLConnection response = httpGet("/foo");
assert response.getResponseCode() == 302;
assert response.getHeaderField("Location").equals("/plantuml/png/oqbDJyrBuGh8ISmh2VNrKGZ8JCuFJqqAJYqgIotY0aefG5G00000");
}
//
// Test DSL
//
private static HttpURLConnection httpGet(String path) throws Exception {
return urlConnection(path);
}
private static HttpURLConnection httpPost(String path, String contentType, byte[] content) throws Exception {
HttpURLConnection conn = urlConnection(path);
conn.setRequestMethod("POST");
conn.setRequestProperty("Content-Type", contentType);
conn.setRequestProperty("Content-Length", Integer.toString(content.length));
conn.setDoOutput(true);
conn.getOutputStream().write(content);
return conn;
}
private static HttpURLConnection httpPostJson(String path, String json) throws Exception {
return httpPost(path, "application/json; utf-8", json.getBytes(UTF_8));
}
private static String httpRaw(String request) throws Exception {
try (Socket socket = socketConnection()) {
socket.getOutputStream().write(request.getBytes(UTF_8));
socket.shutdownOutput();
return readStreamAsString(socket.getInputStream())
.replaceAll("\r\n", "\n");
}
}
private static BufferedImage readStreamAsImage(InputStream in) throws Exception {
return ImageIO.read(new MemoryCacheImageInputStream(in));
}
private static String readStreamAsString(InputStream in) throws IOException {
byte[] buffer = new byte[1024];
int length;
final ByteArrayOutputStream baos = new ByteArrayOutputStream();
while ((length = in.read(buffer)) != -1) {
baos.write(buffer, 0, length);
}
return baos.toString(UTF_8.name());
}
private static String renderRequestJson(String source, String... options) {
final JsonObject object = Json.object();
object.add("source", source);
if (options.length != 0) {
object.add("options", Json.array(options));
}
return object.toString();
}
//
// System under test
//
private static int port;
private static void startServer() throws Exception {
final ServerSocket serverSocket = new ServerSocket(0);
port = serverSocket.getLocalPort();
Thread serverLoopThread = new Thread("PicoWebServerLoop") {
@Override
public void run() {
try {
PicoWebServer.serverLoop(serverSocket);
} catch (IOException e) {
e.printStackTrace();
}
}
};
serverLoopThread.setDaemon(true);
serverLoopThread.start();
}
private static Socket socketConnection() throws IOException {
return new Socket("localhost", port);
}
private static HttpURLConnection urlConnection(String path) throws Exception {
final HttpURLConnection conn = (HttpURLConnection) new URL("http://localhost:" + port + path).openConnection();
conn.setInstanceFollowRedirects(false);
return conn;
}
}

View File

@ -0,0 +1,111 @@
package net.sourceforge.plantuml.picoweb;
import java.io.IOException;
import java.io.InputStream;
import java.util.StringTokenizer;
public class ReceivedHTTPRequest {
private static final String CONTENT_LENGTH_HEADER = "content-length: ";
private String method;
private String path;
private byte[] body;
public String getMethod() {
return method;
}
public String getPath() {
return path;
}
public byte[] getBody() {
return body;
}
public static ReceivedHTTPRequest fromStream(InputStream in) throws IOException {
final ReceivedHTTPRequest request = new ReceivedHTTPRequest();
final String requestLine = readLine(in);
final StringTokenizer tokenizer = new StringTokenizer(requestLine);
if (tokenizer.countTokens() != 3) {
throw new BadRequest400("Bad request line");
}
request.method = tokenizer.nextToken().toUpperCase();
request.path = tokenizer.nextToken();
// Headers
int contentLength = 0;
while (true) {
String line = readLine(in);
if (line.isEmpty()) {
break;
} else if (line.toLowerCase().startsWith(CONTENT_LENGTH_HEADER)) {
contentLength = parseContentLengthHeader(line);
}
}
request.body = readBody(in, contentLength);
return request;
}
private static int parseContentLengthHeader(String line) throws IOException {
int contentLength;
try {
contentLength = Integer.parseInt(line.substring(CONTENT_LENGTH_HEADER.length()).trim());
} catch (NumberFormatException e) {
throw new BadRequest400("Invalid content length");
}
if (contentLength < 0) {
throw new BadRequest400("Negative content length");
}
return contentLength;
}
private static byte[] readBody(InputStream in, int contentLength) throws IOException {
if (contentLength == 0) {
return new byte[0];
}
final byte[] body = new byte[contentLength];
int n = 0;
int offset = 0;
// java.io.InputStream.readNBytes() can replace this from Java 9
while (n < contentLength) {
int count = in.read(body, offset + n, contentLength - n);
if (count < 0) {
throw new BadRequest400("Body too short");
}
n += count;
}
return body;
}
private static String readLine(InputStream in) throws IOException {
final StringBuilder builder = new StringBuilder();
while (true) {
int c = in.read();
if (c == -1 || c == '\n') {
break;
}
builder.append((char) c);
}
if (builder.length() > 0 && builder.charAt(builder.length() - 1) == '\r') {
builder.deleteCharAt(builder.length() - 1);
}
return builder.toString();
}
}

View File

@ -0,0 +1,45 @@
package net.sourceforge.plantuml.picoweb;
import net.sourceforge.plantuml.json.Json;
import net.sourceforge.plantuml.json.JsonArray;
import net.sourceforge.plantuml.json.JsonObject;
/**
* POJO of the json sent to "POST /render"
*/
public class RenderRequest {
private final String[] options;
private final String source;
public RenderRequest(String[] options, String source) {
this.options = options;
this.source = source;
}
public String[] getOptions() {
return options;
}
public String getSource() {
return source;
}
public static RenderRequest fromJson(String json) {
final JsonObject parsed = Json.parse(json).asObject();
final String[] options;
if (parsed.contains("options")) {
final JsonArray jsonArray = parsed.get("options").asArray();
options = new String[jsonArray.size()];
for (int i = 0; i < jsonArray.size(); i++) {
options[i] = jsonArray.get(i).asString();
}
} else {
options = new String[0];
}
return new RenderRequest(options, parsed.get("source").asString());
}
}