C4Graph.java
package org.thewonderlemming.c4plantuml.graphml;
import java.nio.charset.Charset;
import java.nio.file.Path;
import java.util.List;
import java.util.Optional;
import org.antlr.v4.runtime.Parser;
import org.antlr.v4.runtime.tree.ParseTree;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.thewonderlemming.c4plantuml.grammars.SourceType;
import org.thewonderlemming.c4plantuml.grammars.generated.C4L1Parser;
import org.thewonderlemming.c4plantuml.grammars.generated.C4L2Parser;
import org.thewonderlemming.c4plantuml.grammars.generated.C4L3Parser;
import org.thewonderlemming.c4plantuml.graphml.export.GraphMLModelExporter;
import org.thewonderlemming.c4plantuml.graphml.model.C4Keys;
import org.thewonderlemming.c4plantuml.graphml.model.GraphMLModel;
/**
* The C4 to GraphML transformation engine.
* <p>
* It relies on JAXB for the XML marshalling.
*
* @author thewonderlemming
*
*/
public class C4Graph {
private static final Logger LOGGER = LoggerFactory.getLogger(C4Graph.class);
private final Charset charset;
private final boolean formatOutput;
private final GraphMLModel model;
private final C4GraphParseTreeListener parserTreeListener;
private final boolean strictValidation;
private final boolean validateOutput;
private static Optional<ParseTree> createParseTree(final Parser parser) {
final ParseTree tree;
if (parser instanceof C4L1Parser) {
tree = ((C4L1Parser) parser).diagram();
} else if (parser instanceof C4L2Parser) {
tree = ((C4L2Parser) parser).diagram();
} else if (parser instanceof C4L3Parser) {
tree = ((C4L3Parser) parser).diagram();
} else {
return Optional.empty();
}
return Optional.of(tree);
}
private static int sortPathAlphabetically(final Path left, final Path right) {
return String.CASE_INSENSITIVE_ORDER.compare(left.toString(), right.toString());
}
/**
* A constructor that turns on a strict validation of the XML output and that turns off the C4 parsing errors.
*
* @param charset the {@link Charset} to use to read the C4 files.
* @param formatOutput a flag to tell whether the output XML should fit on a single line or not.
*/
public C4Graph(final Charset charset, final boolean formatOutput) {
this(charset, formatOutput, true, true, false);
}
/**
* A constructor that turns on a strict validation of the XML output.
*
* @param charset the {@link Charset} to use to read the C4 files.
* @param formatOutput a flag to tell whether the output XML should fit on a single line or not.
* @param thrownOnParseError a flag to tell whether a parsing error should fail the transformation.
*/
public C4Graph(final Charset charset, final boolean formatOutput, final boolean thrownOnParseError) {
this(charset, formatOutput, true, true, thrownOnParseError);
}
/**
* Default constructor.
*
* @param charset the {@link Charset} to use to read the C4 files.
* @param formatOutput a flag to tell whether the output XML should fit on a single line or not.
* @param validateOutput a flag to tell whether the output XML should be validated against the GraphML XSDs.
* @param strictValidation a flag to tell whether a validation error should fail the transformation.
* @param thrownOnParseError a flag to tell whether a parsing error should fail the transformation.
*/
public C4Graph(final Charset charset, final boolean formatOutput, final boolean validateOutput,
final boolean strictValidation, final boolean thrownOnParseError) {
this.charset = charset;
this.formatOutput = formatOutput;
this.validateOutput = validateOutput;
this.strictValidation = strictValidation;
this.model = GraphMLModel
.builder()
.withKeys(C4Keys.getC4Keys())
.withoutGraphs()
.build();
this.parserTreeListener = new C4GraphParseTreeListener(this.model, thrownOnParseError);
}
/**
* Adds a {@link List} of C4 files to parse and returns the current instance to allow method chaining.
* <p>
* It is better to call that method with every C4 file to parse at once, because they will be sorted alphabetically,
* while if you call that method more than once per generation, you should take responsibility of the ordering of
* these source files.
*
* @param c4Files the {@link List} of C4 files to parse.
* @return the current instance of {@link C4Graph} to allow method chaining.
*/
public C4Graph addAndProcessC4Files(final List<Path> c4Files) {
c4Files
.stream()
.sorted(C4Graph::sortPathAlphabetically)
.forEach(this::processC4File);
return this;
}
/**
* Transforms the parsed C4 files to a single GraphML output and returns the result as a string.
*
* @return the XML output GraphML that represents the parsed C4 files. Returns empty if any silent error occurred.
* @throws Throwable on parsing errors and/or validation errors, depending on the values of the flags in the
* constructor.
*/
public Optional<String> export() throws Throwable {
this.parserTreeListener.wrapUp();
return GraphMLModelExporter
.export(this.model, this.charset, this.formatOutput, this.validateOutput, this.strictValidation);
}
private void processC4File(final Path c4File) {
final Optional<SourceType> optionalSourceType = SourceType.getSourceTypeFromFilename(c4File);
if (!optionalSourceType.isPresent()) {
LOGGER.error("Cannot determine the C4 level from filename: {}. Skipping", c4File);
}
optionalSourceType
.ifPresent(sourceType -> sourceType
.createParser(c4File, charset, C4ErrorListener.getInstance())
.ifPresent(parser -> createParseTree(parser)
.ifPresent(tree -> this.parserTreeListener.parseTree(tree, c4File))));
}
}