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:
parent
7b784cd2f6
commit
a4553adeb2
@ -60,8 +60,37 @@ import net.sourceforge.plantuml.ugraphic.UFont;
|
||||
*
|
||||
*/
|
||||
public enum FileFormat {
|
||||
PNG, SVG, EPS, EPS_TEXT, ATXT, UTXT, XMI_STANDARD, XMI_STAR, XMI_ARGO, SCXML, PDF, MJPEG, ANIMATED_GIF, HTML, HTML5,
|
||||
VDX, LATEX, LATEX_NO_PREAMBLE, BASE64, BRAILLE_PNG, PREPROC;
|
||||
PNG("image/png"),
|
||||
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.
|
||||
|
@ -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"),
|
||||
fileFormatOption.isUseRedForError());
|
||||
HColor backcolor = error.getBackcolor();
|
||||
@ -233,7 +233,7 @@ public class SourceStringReader {
|
||||
null, ClockwiseTopRightBottomLeft.none(), backcolor);
|
||||
final ImageBuilder imageBuilder = ImageBuilder.build(imageParameter);
|
||||
imageBuilder.setUDrawable(error);
|
||||
imageBuilder.writeImageTOBEMOVED(fileFormatOption, seed, os);
|
||||
return imageBuilder.writeImageTOBEMOVED(fileFormatOption, seed, os);
|
||||
}
|
||||
|
||||
public final List<BlockUml> getBlocks() {
|
||||
|
14
src/net/sourceforge/plantuml/picoweb/BadRequest400.java
Normal file
14
src/net/sourceforge/plantuml/picoweb/BadRequest400.java
Normal 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);
|
||||
}
|
||||
}
|
@ -36,34 +36,40 @@
|
||||
*/
|
||||
package net.sourceforge.plantuml.picoweb;
|
||||
|
||||
import java.io.BufferedInputStream;
|
||||
import java.io.BufferedOutputStream;
|
||||
import java.io.BufferedReader;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.OutputStream;
|
||||
import java.io.PrintWriter;
|
||||
import java.net.InetAddress;
|
||||
import java.net.ServerSocket;
|
||||
import java.net.Socket;
|
||||
import java.util.Collections;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.StringTokenizer;
|
||||
|
||||
import net.sourceforge.plantuml.BlockUml;
|
||||
import net.sourceforge.plantuml.ErrorUml;
|
||||
import net.sourceforge.plantuml.FileFormat;
|
||||
import net.sourceforge.plantuml.FileFormatOption;
|
||||
import net.sourceforge.plantuml.LineLocationImpl;
|
||||
import net.sourceforge.plantuml.Option;
|
||||
import net.sourceforge.plantuml.SourceStringReader;
|
||||
import net.sourceforge.plantuml.StringLocated;
|
||||
import net.sourceforge.plantuml.StringUtils;
|
||||
import net.sourceforge.plantuml.code.NoPlantumlCompressionException;
|
||||
import net.sourceforge.plantuml.code.Transcoder;
|
||||
import net.sourceforge.plantuml.code.TranscoderUtil;
|
||||
import net.sourceforge.plantuml.core.Diagram;
|
||||
import net.sourceforge.plantuml.core.ImageData;
|
||||
import net.sourceforge.plantuml.error.PSystemError;
|
||||
import net.sourceforge.plantuml.error.PSystemErrorUtils;
|
||||
import net.sourceforge.plantuml.graphic.QuoteUtils;
|
||||
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 {
|
||||
|
||||
private final Socket connect;
|
||||
@ -80,6 +86,10 @@ public class PicoWebServer implements Runnable {
|
||||
final InetAddress bindAddress1 = bindAddress == null ? null : InetAddress.getByName(bindAddress);
|
||||
final ServerSocket serverConnect = new ServerSocket(port, 50, bindAddress1);
|
||||
System.err.println("webPort=" + serverConnect.getLocalPort());
|
||||
serverLoop(serverConnect);
|
||||
}
|
||||
|
||||
public static void serverLoop(final ServerSocket serverConnect) throws IOException {
|
||||
while (true) {
|
||||
final PicoWebServer myServer = new PicoWebServer(serverConnect.accept());
|
||||
final Thread thread = new Thread(myServer);
|
||||
@ -88,30 +98,25 @@ public class PicoWebServer implements Runnable {
|
||||
}
|
||||
|
||||
public void run() {
|
||||
BufferedReader in = null;
|
||||
BufferedInputStream in = null;
|
||||
BufferedOutputStream out = null;
|
||||
|
||||
try {
|
||||
in = new BufferedReader(new InputStreamReader(connect.getInputStream(), "UTF-8"));
|
||||
in = new BufferedInputStream(connect.getInputStream());
|
||||
out = new BufferedOutputStream(connect.getOutputStream());
|
||||
|
||||
final String first = in.readLine();
|
||||
if (first == null) {
|
||||
final ReceivedHTTPRequest request = ReceivedHTTPRequest.fromStream(in);
|
||||
if (request.getMethod().equals("GET")) {
|
||||
if (request.getPath().startsWith("/png/") && handleGET(request, out, FileFormat.PNG))
|
||||
return;
|
||||
}
|
||||
|
||||
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))
|
||||
if (request.getPath().startsWith("/plantuml/png/") && handleGET(request, out, FileFormat.PNG))
|
||||
return;
|
||||
if (path.startsWith("/plantuml/png/") && sendDiagram(out, path, "image/png", FileFormat.PNG))
|
||||
if (request.getPath().startsWith("/svg/") && handleGET(request, out, FileFormat.SVG))
|
||||
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;
|
||||
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;
|
||||
}
|
||||
write(out, "HTTP/1.1 302 Found");
|
||||
@ -120,7 +125,12 @@ public class PicoWebServer implements Runnable {
|
||||
out.flush();
|
||||
|
||||
} catch (Throwable e) {
|
||||
try {
|
||||
sendError(e, out);
|
||||
}
|
||||
catch (Throwable e1) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
} finally {
|
||||
try {
|
||||
in.close();
|
||||
@ -132,26 +142,74 @@ public class PicoWebServer implements Runnable {
|
||||
}
|
||||
}
|
||||
|
||||
private boolean sendDiagram(BufferedOutputStream out, String path, final String mime, final FileFormat format)
|
||||
throws NoPlantumlCompressionException, IOException {
|
||||
final int x = path.lastIndexOf('/');
|
||||
final String compressed = path.substring(x + 1);
|
||||
private boolean handleGET(ReceivedHTTPRequest request, BufferedOutputStream out, final FileFormat format) throws IOException {
|
||||
final int x = request.getPath().lastIndexOf('/');
|
||||
final String compressed = request.getPath().substring(x + 1);
|
||||
final Transcoder transcoder = TranscoderUtil.getDefaultTranscoderProtected();
|
||||
final String source = transcoder.decode(compressed);
|
||||
final SourceStringReader ssr = new SourceStringReader(source);
|
||||
|
||||
final List<BlockUml> blocks = ssr.getBlocks();
|
||||
if (blocks.size() > 0) {
|
||||
final FileFormatOption fileFormatOption = new FileFormatOption(format);
|
||||
final Diagram system = blocks.get(0).getDiagram();
|
||||
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();
|
||||
final byte[] fileData = os.toByteArray();
|
||||
write(out, "HTTP/1.1 " + httpReturnCode(imageData.getStatus()));
|
||||
sendDiagram(out, system, fileFormatOption, httpReturnCode(imageData.getStatus()), imageData, os.toByteArray());
|
||||
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, "Server: PlantUML PicoWebServer " + Version.versionString());
|
||||
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, "X-PlantUML-Diagram-Width: " + imageData.getWidth());
|
||||
write(out, "X-PlantUML-Diagram-Height: " + imageData.getHeight());
|
||||
@ -170,9 +228,25 @@ public class PicoWebServer implements Runnable {
|
||||
out.flush();
|
||||
out.write(fileData);
|
||||
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) {
|
||||
|
316
src/net/sourceforge/plantuml/picoweb/PicoWebServerTest.java
Normal file
316
src/net/sourceforge/plantuml/picoweb/PicoWebServerTest.java
Normal 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;
|
||||
}
|
||||
}
|
111
src/net/sourceforge/plantuml/picoweb/ReceivedHTTPRequest.java
Normal file
111
src/net/sourceforge/plantuml/picoweb/ReceivedHTTPRequest.java
Normal 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();
|
||||
}
|
||||
}
|
45
src/net/sourceforge/plantuml/picoweb/RenderRequest.java
Normal file
45
src/net/sourceforge/plantuml/picoweb/RenderRequest.java
Normal 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());
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user