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 {
|
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.
|
||||||
|
@ -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() {
|
||||||
|
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;
|
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) {
|
||||||
|
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