Skip to content

Commit

Permalink
Merge pull request #692 from Mogztter/issue-689-delegate-ditaa
Browse files Browse the repository at this point in the history
resolves #689 delegate to ditaa
  • Loading branch information
ggrossetie authored Apr 13, 2021
2 parents c1007d7 + 7f4a35f commit 8cc9377
Show file tree
Hide file tree
Showing 4 changed files with 277 additions and 10 deletions.
20 changes: 20 additions & 0 deletions server/src/main/java/io/kroki/server/action/DitaaContext.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package io.kroki.server.action;

public class DitaaContext {

private final String source;
private final String options;

public DitaaContext(String source, String options) {
this.source = source;
this.options = options;
}

public String getSource() {
return source;
}

public String getOptions() {
return options;
}
}
16 changes: 16 additions & 0 deletions server/src/main/java/io/kroki/server/log/Logging.java
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,22 @@ public void convert(RoutingContext routingContext, long start, String serviceNam
}
}

public void reroute(String from, String to, String source, FileFormat fileFormat) {
try {
MDC.put("action", "reroute");
MDC.put("service_name_from", from);
MDC.put("service_name_to", to);
MDC.put("source", source);
MDC.put("file_format", fileFormat.getName());
logger.info("Reroute");
} finally {
MDC.remove("action");
MDC.remove("service_name_from");
MDC.remove("service_name_to");
MDC.remove("file_format");
}
}

public void error(RoutingContext routingContext, int errorCode, String errorMessage) {
HttpServerRequest request = routingContext.request();
Throwable failure = routingContext.failure();
Expand Down
65 changes: 57 additions & 8 deletions server/src/main/java/io/kroki/server/service/Plantuml.java
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
package io.kroki.server.service;

import io.kroki.server.action.Delegator;
import io.kroki.server.action.DitaaContext;
import io.kroki.server.decode.DiagramSource;
import io.kroki.server.decode.SourceDecoder;
import io.kroki.server.error.BadRequestException;
import io.kroki.server.error.DecodeException;
import io.kroki.server.format.FileFormat;
import io.kroki.server.log.Logging;
import io.kroki.server.security.SafeMode;
import io.kroki.server.unit.TimeValue;
import io.vertx.core.AsyncResult;
Expand All @@ -27,6 +29,7 @@
import org.slf4j.LoggerFactory;

import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
Expand Down Expand Up @@ -75,13 +78,16 @@ public class Plantuml implements DiagramService {
* @see <a href="https://plantuml.com/preprocessing">PlantUML Preprocessing</a>
*/
public static final Pattern INCLUDE_RX = Pattern.compile("^\\s*!include(?:url|sub)?\\s+(?<path>(?:(?<=\\\\)[ ]|[^ ])+)(.*)");
private static final Pattern STARTDITAA_BLOCK_RX = Pattern.compile("(?:^| +)@startditaa(?<opts>\\s\\S*)\\n(?<source>.*?) *@endditaa", Pattern.MULTILINE | Pattern.DOTALL);
private static final Pattern DITAA_KEYWORD_BOCK_RX = Pattern.compile("@startuml(:?\\s*)(?:^| +)ditaa(?:(?:\\((?<opts>[^)]*)\\))?| *)\\n(?<source>.*?) *@end(?:ditaa|uml)", Pattern.MULTILINE | Pattern.DOTALL);

private static final Logger logger = LoggerFactory.getLogger(Plantuml.class);
private static final List<FileFormat> SUPPORTED_FORMATS = Arrays.asList(FileFormat.PNG, FileFormat.SVG, FileFormat.JPEG, FileFormat.BASE64, FileFormat.TXT, FileFormat.UTXT);
private static final Pattern STDLIB_PATH_RX = Pattern.compile("<([a-zA-Z0-9]+)/[^>]+>");

private final Vertx vertx;
private final SafeMode safeMode;
private final Logging logging;
private static final String c4 = read("c4.puml");
// context includes c4
private static final String c4Context = c4 + read("c4_context.puml");
Expand Down Expand Up @@ -121,6 +127,7 @@ public String decode(String encoded) throws DecodeException {
}
};
this.includeWhitelist = parseIncludeWhitelist(config);
this.logging = new Logging(logger);
// Disable unsafe include for security reasons
OptionFlags.ALLOW_INCLUDE = config.getBoolean("KROKI_PLANTUML_ALLOW_INCLUDE", false);
String plantUmlIncludePath = config.getString("KROKI_PLANTUML_INCLUDE_PATH");
Expand Down Expand Up @@ -196,6 +203,7 @@ public void convert(String sourceDecoded, String serviceName, FileFormat fileFor
String source;
try {
source = sanitize(sourceDecoded, this.safeMode, this.includeWhitelist);
source = source.trim();
source = withDelimiter(source);
} catch (IOException e) {
if (e instanceof UnsupportedEncodingException) {
Expand All @@ -206,14 +214,31 @@ public void convert(String sourceDecoded, String serviceName, FileFormat fileFor
return;
}
final String primeSource = source;
vertx.executeBlocking(future -> {
try {
byte[] data = convert(primeSource, fileFormat);
future.complete(data);
} catch (IllegalStateException e) {
future.fail(e);
}
}, res -> handler.handle(res.map(o -> Buffer.buffer((byte[]) o))));
DitaaContext ditaaContext = findDitaaContext(source);
if (ditaaContext != null) {
logging.reroute("plantuml", "ditaa", ditaaContext.getSource(), fileFormat);
// found a ditaa context, delegate to the optimized ditaa service
vertx.executeBlocking(future -> {
try {
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
// REMIND: options are unsupported for now.
Ditaa.convert(fileFormat, new ByteArrayInputStream(ditaaContext.getSource().getBytes()), outputStream);
future.complete(outputStream.toByteArray());
} catch (IllegalStateException e) {
future.fail(e);
}
}, res -> handler.handle(res.map(o -> Buffer.buffer((byte[]) o))));
} else {
// ...otherwise, continue with PlantUML
vertx.executeBlocking(future -> {
try {
byte[] data = convert(primeSource, fileFormat);
future.complete(data);
} catch (IllegalStateException e) {
future.fail(e);
}
}, res -> handler.handle(res.map(o -> Buffer.buffer((byte[]) o))));
}
}

static byte[] convert(String source, FileFormat format) {
Expand Down Expand Up @@ -371,4 +396,28 @@ private static String read(String resource) {
throw new RuntimeException("Unable to initialize the C4 PlantUML service", e);
}
}

/**
* Try to find a ditaa context from a PlantUML source.
* @param source PlantUML source
* @return a {@link DitaaContext} or null if not found
*/
static DitaaContext findDitaaContext(String source) {
if (source.contains("@startditaa") && source.contains("@endditaa")) {
Matcher ditaablockMatcher = STARTDITAA_BLOCK_RX.matcher(source);
if (ditaablockMatcher.find()) {
String ditaaOptions = ditaablockMatcher.group("opts");
String ditaaSource = ditaablockMatcher.group("source");
return new DitaaContext(ditaaSource, ditaaOptions);
}
} else {
Matcher ditaaMatcher = DITAA_KEYWORD_BOCK_RX.matcher(source);
if (ditaaMatcher.find()) {
String ditaaOptions = ditaaMatcher.group("opts");
String ditaaSource = ditaaMatcher.group("source");
return new DitaaContext(ditaaSource, ditaaOptions);
}
}
return null;
}
}
186 changes: 184 additions & 2 deletions server/src/test/java/io/kroki/server/service/PlantumlServiceTest.java
Original file line number Diff line number Diff line change
@@ -1,16 +1,17 @@
package io.kroki.server.service;

import io.kroki.server.action.DitaaContext;
import io.kroki.server.error.BadRequestException;
import io.kroki.server.format.FileFormat;
import io.kroki.server.security.SafeMode;
import io.vertx.core.json.JsonObject;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;

import java.io.IOException;
import java.net.URISyntaxException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Paths;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
Expand Down Expand Up @@ -464,4 +465,185 @@ void should_trim_regex_on_whitelist_environment_variables() {
.extracting("pattern")
.containsExactly("/path1", "/path2", "/path3");
}

@ParameterizedTest
@ValueSource(strings = {
"@startuml\nditaa -> hello\n@enduml",
"@startuml\n@starditaa->@endditaa\n@enduml",
"@startuml\n[@startditaa->@endditaa\n@enduml",
"@startuml\nBob->Alice: \\\nditaa\nFoo->Bar\n@enduml",
})
void should_not_find_ditaa_context(String input) {
DitaaContext ditaaContext = Plantuml.findDitaaContext(input);
assertThat(ditaaContext).isNull();
}

@Test
void should_find_ditaa_context_with_ditaa_keyword() {
DitaaContext ditaaContext = Plantuml.findDitaaContext("@startuml\n" +
"ditaa\n" +
" Input Output\n" +
"\n" +
"+-----------+ +----------+\n" +
"| Client |=------------------->| Kroki.io |\n" +
"+-----------+ +----------+\n" +
"@enduml");
assertThat(ditaaContext).isNotNull();
assertThat(ditaaContext.getOptions()).isNull();
assertThat(ditaaContext.getSource()).isEqualTo(" Input Output\n" +
"\n" +
"+-----------+ +----------+\n" +
"| Client |=------------------->| Kroki.io |\n" +
"+-----------+ +----------+\n"
);
}

@Test
void should_find_ditaa_context_with_ditaa_keyword_empty_opts() {
DitaaContext ditaaContext = Plantuml.findDitaaContext("@startuml\n" +
"ditaa()\n" +
" Input Output\n" +
"\n" +
"+-----------+ +----------+\n" +
"| Client |=------------------->| Kroki.io |\n" +
"+-----------+ +----------+\n" +
"@enduml");
assertThat(ditaaContext).isNotNull();
assertThat(ditaaContext.getOptions()).isEqualTo("");
assertThat(ditaaContext.getSource()).isEqualTo(" Input Output\n" +
"\n" +
"+-----------+ +----------+\n" +
"| Client |=------------------->| Kroki.io |\n" +
"+-----------+ +----------+\n"
);
}

@Test
void should_find_ditaa_context_with_ditaa_keyword_opts() {
DitaaContext ditaaContext = Plantuml.findDitaaContext("@startuml\n" +
"ditaa(--no-shadows, scale=0.7)\n" +
" Input Output\n" +
"\n" +
"+-----------+ +----------+\n" +
"| Client |=------------------->| Kroki.io |\n" +
"+-----------+ +----------+\n" +
"@enduml");
assertThat(ditaaContext).isNotNull();
assertThat(ditaaContext.getOptions()).isEqualTo("--no-shadows, scale=0.7");
assertThat(ditaaContext.getSource()).isEqualTo(" Input Output\n" +
"\n" +
"+-----------+ +----------+\n" +
"| Client |=------------------->| Kroki.io |\n" +
"+-----------+ +----------+\n"
);
}

@Test
void should_find_ditaa_context_with_startditaa_opts() {
DitaaContext ditaaContext = Plantuml.findDitaaContext("@startditaa -E\n" +
" Input Output\n" +
"\n" +
"+-----------+ +----------+\n" +
"| Client |=------------------->| Kroki.io |\n" +
"+-----------+ +----------+\n" +
"@endditaa");
assertThat(ditaaContext).isNotNull();
assertThat(ditaaContext.getOptions()).isEqualTo(" -E");
assertThat(ditaaContext.getSource()).isEqualTo(" Input Output\n" +
"\n" +
"+-----------+ +----------+\n" +
"| Client |=------------------->| Kroki.io |\n" +
"+-----------+ +----------+\n"
);
}

@Test
void should_extract_startditaa_block() {
DitaaContext ditaaContext = Plantuml.findDitaaContext("@startuml\n" +
"\n" +
" @startditaa -E\n" +
" Input Output\n" +
"\n" +
"+-----------+ +----------+\n" +
"| Client |=------------------->| Kroki.io |\n" +
"+-----------+ +----------+\n" +
"\n " +
"@endditaa\n" +
"" +
"" +
"" +
"" +
"" +
"@endditaa\n" +
"@enduml");
assertThat(ditaaContext).isNotNull();
assertThat(ditaaContext.getOptions()).isEqualTo(" -E");
assertThat(ditaaContext.getSource()).isEqualTo(" Input Output\n" +
"\n" +
"+-----------+ +----------+\n" +
"| Client |=------------------->| Kroki.io |\n" +
"+-----------+ +----------+\n" +
"\n"
);
}

@Test
void should_extract_ditaa_keyword_block() {
DitaaContext ditaaContext = Plantuml.findDitaaContext("@startuml\n" +
"\n" +
" ditaa(-E)\n" +
" Input Output\n" +
"\n" +
"+-----------+ +----------+\n" +
"| Client |=------------------->| Kroki.io |\n" +
"+-----------+ +----------+\n" +
"\n " +
"@endditaa\n" +
"" +
"" +
"" +
"" +
"" +
"@endditaa\n" +
"@enduml");
assertThat(ditaaContext).isNotNull();
assertThat(ditaaContext.getOptions()).isEqualTo("-E");
assertThat(ditaaContext.getSource()).isEqualTo(" Input Output\n" +
"\n" +
"+-----------+ +----------+\n" +
"| Client |=------------------->| Kroki.io |\n" +
"+-----------+ +----------+\n" +
"\n"
);
}

@Test
void should_extract_ditaa_keyword_block_without_opts() {
DitaaContext ditaaContext = Plantuml.findDitaaContext("@startuml\n" +
"\n" +
" ditaa \n" +
" Input Output\n" +
"\n" +
"+-----------+ +----------+\n" +
"| Client |=------------------->| Kroki.io |\n" +
"+-----------+ +----------+\n" +
"\n " +
"@endditaa\n" +
"" +
"" +
"" +
"" +
"" +
"@endditaa\n" +
"@enduml");
assertThat(ditaaContext).isNotNull();
assertThat(ditaaContext.getOptions()).isNull();
assertThat(ditaaContext.getSource()).isEqualTo(" Input Output\n" +
"\n" +
"+-----------+ +----------+\n" +
"| Client |=------------------->| Kroki.io |\n" +
"+-----------+ +----------+\n" +
"\n"
);
}
}

0 comments on commit 8cc9377

Please sign in to comment.