Files
exercise05/MicroJava Compiler/src/ssw/mj/Visualizer.java
2025-11-08 15:33:53 +01:00

219 lines
7.7 KiB
Java

package ssw.mj;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import ssw.mj.scanner.Token;
import java.awt.*;
import java.io.IOException;
import java.lang.reflect.Field;
import java.net.URI;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.LocalDateTime;
import java.util.List;
public class Visualizer {
// Config Flags
private static final boolean debug = false;
private static final String visualizerUrl = "https://ssw.jku.at/General/Staff/Weninger/Teaching/CB/Visualization/v1_0";
// private static final String visualizerUrl = "http://localhost:5173/General/Staff/Weninger/Teaching/CB/Visualization/v1_0";
public static final Gson gson = debug ? new GsonBuilder().setPrettyPrinting().create() : new Gson();
// Remember when the browser was last opened, to ensure that we
// don't open a huge number of tabs
private static LocalDateTime browserLastOpened;
public static class VisualizerException extends RuntimeException {
public VisualizerException(String message) {
super("[Recorder]: " + message);
}
}
private interface WithDescription {
String getDescription();
}
public enum VisualizationType implements WithDescription {
SCANNER_V1 {
@Override
public String getDescription() {
return "Scanner Visualization";
}
},
PARSER_V1 {
@Override
public String getDescription() {
return "Parser Visualization";
}
}
}
private record ScannerPayload(List<Token> receivedTokens, List<Token> expectedTokens) {
public String toJson() {
return gson.toJson(this);
}
}
private record ParserPayload(List<Recorder.RecorderTraceEntry> traces) {
public String toJson() {
return gson.toJson(this);
}
}
/**
* Transforms scanner tokens to our visualizer format and ingests them to the desired method
*
* @param receivedTokens The tokens received from the student's scanner
* @param expectedTokens The tokens expected by the test case
*/
public static void createScannerVisualization(String sourcecode,
List<Token> receivedTokens,
List<Token> expectedTokens,
boolean openInBrowserAfterwards) {
String payload = new ScannerPayload(receivedTokens, expectedTokens).toJson();
VisualizerIngestData visData = new VisualizerIngestData(VisualizationType.SCANNER_V1, sourcecode, payload);
Path path = ingestFile(visData);
if (openInBrowserAfterwards) {
openBrowser(path.toUri().toString());
}
}
/**
* Get the secret recorder from the parser and ingest the tokens to the visualizer
*/
public static void createParserVisualization(String sourcecode, boolean openInBrowserAfterwards) {
Recorder recorder = getRecorderFromParserUsingReflection();
// If we don't have a recorder, we can't create a visualization
if (recorder == null) return;
String payload = new ParserPayload(recorder.getTrace()).toJson();
VisualizerIngestData visData = new VisualizerIngestData(VisualizationType.PARSER_V1, sourcecode, payload);
Path path = ingestFile(visData);
if (openInBrowserAfterwards) {
openBrowser(path.toUri().toString());
}
}
// Data to be sent to the ingest endpoint
private record VisualizerIngestData(VisualizationType type, String sourcecode, String payload) {
public String toJson() {
return gson.toJson(this);
}
}
/**
* Create a visualization file
*
* @param data The visualization data
* @return The path at which the visualization file has been created
*/
private static Path ingestFile(VisualizerIngestData data) {
String html = "<!DOCTYPE html>\n" +
"<html lang=\"en\">\n" +
" <head>\n" +
" <meta charset=\"UTF-8\">\n" +
" <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n" +
" <title>Compiler Visualization</title>\n" +
" </head>\n" +
" <body>\n" +
" <div id=\"app\"></div>\n" +
"\n" +
" <script id=\"__DATA__\" type=\"application/json\">\n" +
" " + (data.toJson()) + "\n" + // Inject the visualization data
" </script>\n" +
"\n" +
" <script type=\"text/javascript\">\n" +
" let data = JSON.parse(document.getElementById('__DATA__').textContent);\n" +
" let app = document.getElementById('app');\n" +
"\n" +
" let iframe = document.createElement('iframe');\n" +
" iframe.src = '" + visualizerUrl + "';\n" +
" iframe.style.position = 'fixed';\n" +
" iframe.style.top = 0;\n" +
" iframe.style.left = 0;\n" +
" iframe.style.right = 0;\n" +
" iframe.style.bottom = 0;\n" +
" iframe.style.border = 0;\n" +
" iframe.style.width = '100%';\n" +
" iframe.style.height = '100%';\n" +
"\n" +
" app.appendChild(iframe);\n" +
" \n" +
" iframe.addEventListener('load', () => {\n" +
" setTimeout(() => {\n" +
" iframe.contentWindow.postMessage({\n" +
" type: 'boot',\n" +
" data\n" +
" }, '*');\n" +
" }, 100);\n" +
" });\n" +
" </script>\n" +
" </body>\n" +
"</html>";
try {
// Get a temporary file to write the visualization to
Path path = Files.createTempFile("microjava-visualization", ".html");
Files.writeString(path, html);
System.out.println(data.type.getDescription() + " created successfully. View it at: " + path.toUri().toString());
return path;
} catch (IOException e) {
throw new VisualizerException("Failed to create visualization: " + e.getMessage());
}
}
/**
* Try to open the browser if possible and not rate limited
*
* @param url The url to open
*/
private static void openBrowser(String url) {
if (browserLastOpened != null && browserLastOpened.plusSeconds(5).isAfter(LocalDateTime.now())) {
return;
}
if (Desktop.isDesktopSupported()) {
try {
Desktop.getDesktop().browse(new URI(url));
} catch (Exception e) {
// Ignore
}
}
}
/**
* Use reflection to get the secret recorder from the parser
*
* @return The recorder
*/
private static Recorder getRecorderFromParserUsingReflection() {
try {
// Get the parser class
Class<?> parserClass = Class.forName("ssw.mj.impl.Parser");
// __recorder__ is a static field that has secretly been added in the Parser class by the TracingClassLoader
// during a run of the application / the unit tests with the CLI setting
// -Djava.system.class.loader=ssw.mj.TracingClassLoader
Field recorderField = parserClass.getField("__recorder__");
// Get the value of the field
Recorder recorder = (Recorder) recorderField.get(null);
return recorder;
} catch (Exception e) {
// throw new VisualizerException("Failed to get recorder from parser: " + e.getMessage());
// We don't want to throw an exception here, since we might not have a recorder
// when the custom class loader is not used
return null;
}
}
}